Make a field optional in a form only when I am editing - symfony-forms

I created a symfony form for two actions (creation and edition) and I want to put one of the fields of the form optional only when I edit

Assuming you're using a data_class and Doctrine:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$object = $options['data'] ?? null;
$isEdit = $object && $object->getId();
$builder->add('name', null, [
'required' => !$isEdit,
...
]);
}
On create, the form either contains no underlying object or an underlying object without an id, since the object has not been saved to the database yet when the form is built.
On edit, the form contains an underlying object with an id.
So, get the underlying object from the form with $options['data'] and check if it is not null and has an id.
If an object with an id exists, we know we're on edit ($isEdit will be true) and can use that to set the required property.

Related

Metadata vs ViewData in HtmlHelper (DropDownListFor Bug?)

I'm experimenting with custom ModelMetadataProvider. It would appear that some of the html helpers like TextBoxFor use these just fine. However, in other cases like DropDownListFor, they favor ViewData instead. For example, looking at some reflected code I see:
bool flag = false;
if (selectList == null)
{
selectList = SelectExtensions.GetSelectData(htmlHelper, name);
flag = true;
}
object defaultValue = allowMultiple ? htmlHelper.GetModelStateValue(fullHtmlFieldName, typeof (string[])) : htmlHelper.GetModelStateValue(fullHtmlFieldName, typeof (string));
if (defaultValue == null && !string.IsNullOrEmpty(name))
{
if (!flag)
defaultValue = htmlHelper.ViewData.Eval(name);
else if (metadata != null)
defaultValue = metadata.Model;
}
Note all the different attempts to get "defaultValue". Using metadata.Model is dead last. Why the separation here? If you trace that code thru you eventually end up at a call to ViewData.Eval, which as a fall back just uses reflection to get the value out of the model anyway. Is there such a thing as a custom ViewData provider to bridge that gap?
EDIT: I'm beginning to lean toward the idea that this is a bug in the framework.
Consider two pieces of code:
#Html.DropDownListFor(model => model.ErrorData.Shift, Model.ShiftOptions, new { #class = "form-control" })
The code above passes in the options "Model.ShiftOptions". Because of this it doesn't pass the condition "selectList==null" and consequently "flag" is never set and instead proceeds to try to get the default value from only the type via reflection (the Eval call).
However with this code:
#{ ViewData[Html.NameFor(m => m.ErrorData.Shift).ToString()] = Model.ShiftOptions;}
#Html.DropDownListFor(model => model.ErrorData.Shift,null, new { #class = "form-control" })
..."flag" is now satisfied and the default value is now retrieved metadata.Model. Why would different mechanisms for providing the list options change (or even influence for that matter) where the default value is retrieved from?
Edit #2
Warning: The above ViewData "fix" does not work if the DropDownListFor is called in an editor template (EditorFor) for a complex type. The NameFor call will return the name of the property INCLUDING the outer context that the EditorFor was called from, ie MyViewModel.ErrorData.Shift. However, the code for DropDownListFor in the orginal snip at the top looks for a ViewData item WITHOUT the original context, ie ErrorData.Shift. They both use
ExpressionHelper.GetExpressionText((LambdaExpression) expression)
However, NameOf uses html.Name on that result. When DDLF finally gets around to generating its name, it does something similar so it's name is correct, but it makes no sense that it doesn't include it's full context when looking for a view data option.
All the HtmlHelper methods for generating form controls first check if there is a value for the property in ModelState (the GetModelStateValue() method) to handle the case where the form has been submitted with an invalid value and the view is returned (refer the 2nd part of this answer for an explanation of why this is the default behavior).
In the case where you use DropDownList(), for example
#Html.DropDownList("xxx", null, "--Please select--")
where xxx is IEnumerable<SelectListItem> that has been added as a ViewBag property, the value of selectList is null and the code in the first if block is executed and the value of flag is true (and note also that the model may or may not have a property named xxx, meaning that metadata might be null)
alternatively, if you used the strongly typed DropDownListFor() method, for example
#Html.DropDownListFor(m => m.SomeProperty, Model.SomePropertyList, "--Please select--")
the value of selectList is not null (assuming that SomePropertyList is IEnumerable<SelectListItem>and is not null) and the value of flag is false
So the various checks are just taking into account the different ways that you can use either DropDownList() or DropDownListFor() to generate a <select> element, and whether you are binding to a model property or not.
Side note: The actual code (from the private static MvcHtmlString SelectInternal() method) is bool usedViewData = false;, not bool flag = false;

asp.net mvc4 controller allow null complex object

If I have this:
public ActionResult BuscarClientes(SomeClass c)
{ ... code ...}
And I access the url to this action without any parameter (so I don't give any elements to my model), I still get a newly created object. But, I'm wanting to get a NULL object instead if no arguments are given.
This is because I'm using this method as a search action method, and the first time the get is done I don't want to perform any validation and just return the view. After that, the post will be a GET method (its a search I need to make it a get request) with all the values in the query string.
How can I force the model binder to give me a null object if no parameters are given in the query string? Because as it is now, i get a new instance of SomeClass with all its properties set to null. Instead of just a null object.
Try specifying default value to the parameter
public ActionResult BuscarClientes(SomeClass c = null)
{ ... code ...}

ModelState binding custom array of checkboxes

ViewModel Binding is working, the object passed back to the edit controller contains the correct values, which is a list of selected options. However, ModelState binding is not working, the model state AttemptedValues exist, but aren't being reloaded into the fields.
I have a model with the following properties
class Model
{
public List<string> AvailableValues { get; set; }
public List<string> SelectedValues { get; set; }
}
But in my view I have some categorization, so I can't do a direct foreach.
foreach (var category in CatgoryList.Categories)
{
foreach (var available in Model.AvailableValues.Where(x => category.AvailableValues.Contains(x))
{
var check = Model.SelectedValues!= null && Model.SelectedValues.Contains(available.Id);
check &= (ViewData.ModelState["SelectedValues"] != null) && ViewData.ModelState["SelectedValues"].Value.AttemptedValue.Contains(available.Id);
<input type="checkbox" name="SelectedValues" id="available.Id" value="available.Id" checked="#check"/>#available.FriendlyName<br>
}
}
The ModelState does contain SelectedValues from the previous post, but it doesn't auto-bind, because I have a custom field for the checkboxes.
This code is smelly
Is there a better way to get the data to load from the Attempted Value
EDIT:
Ok, so my question wasn't clear enough, let me clarify.
On a validate, I'm retuning the same view if there was an error.
The modelstate is holding the previously entered values in ModelState["field"].Value.AttemptedValue.
With fields created using the helpers, TextboxFor, CheckboxFor, etc, these values are automatically filled in.
However, when using the normal reflexes for checkbox binding, only the values of the checked checkboxes are returned in the data object passed back to the controller. This means I'm not using the logic that fills values in from the ModelState.
What I've done is dig through the modelstate myself for the attempted values, because they do exist under the field name "SelectedValues". But I have to manually apply them. The value there looks like this.
ModelState["SelectedValues"] = "Value1;Value2;Value4"
Is there a better way to get the data to load from the Attempted Value in the model state.
The primary "smell" (to use your term) I see here is that the code you have in the nested foreach is written directly in your view (*.cshtml), but code of that complexity should be in your Controller action.
You should calculate and generate all the data your view will need in the controller, and then pass that data through to the view using Model (looks like you are already doing that) and you can also use the ViewBag to pass additional data not contained in your Model. Then the view is just responsible to generate the HTML.
That's the other problem I see with your code - you are referencing the ViewData.ModelState which is highly unusual to see in a view. The ModelState should be examined in the controller before you even decide which view to render.
It looks like maybe you are just passing data through ViewData.ModelState that should actually be passed through ViewData/ViewBag.
You can read more about passing data to a view here.
Ok, so basically, I couldn't find anything that will do this for me. The default Html helper methods just don't cover this scenario.
So, I wrote an extension method.
Basically it pulls in the enumerator from the model using the expression you send to it, just like any other helper, but you also send the entry in the list you want to build a checkbox against.
It ends up looking like this.
#Html.CheckboxListEntryFor(x => x.SelectedEntries, AvailableEntries[i].Id)
The method does the following
Get the propertyInfo for the list and check if selected entries contains the values.
Check if the ModelState is invalid, if so, overwrite the checked value with the modelstate entry
build an html checkbox that uses the property name as the name and id of the checkbox, and sets checked based on the previous steps.
public static MvcHtmlString CheckboxListEntryFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression, string entryValue)
{
PropertyInfo info = GetPropertyInfo(typeof (TModel), expression);
var enumerator = info.GetValue(htmlHelper.ViewData.Model);
var check = enumerator != null && ((IList) enumerator).Contains(entryValue);
if (!htmlHelper.ViewData.ModelState.IsValid)
{
check = htmlHelper.ViewData.ModelState[info.Name] != null &&
htmlHelper.ViewData.ModelState[info.Name].Value.AttemptedValue.Contains(entryValue);
}
var fieldString = String.Format(
"<input type=\"checkbox\" name=\"{0}\" id =\"{1}\" value=\"{1}\"{2}/>",
info.Name, entryValue, check ? " checked=\"checked\"" : string.Empty);
return MvcHtmlString.Create(fieldString);
}

TryUpdateModel - the model of type could not be updated

I'm using Telerik's MVC Grid to edit some records in MVC3, using Razor view.
I call the edit on the controller using the following code:
public ActionResult _CategoriesUpdate(int id)
{
WR_TakeAway_Menu_Categories category = db.WR_TakeAway_Menu_Categories.Where(c => c.ID == id).Single();
TryUpdateModel(category);
db.ApplyCurrentValues(category.EntityKey.EntitySetName, category);
db.ObjectStateManager.ChangeObjectState(category, EntityState.Modified);
db.SaveChanges();
Although this updates the records in the serer, it keeps the grid in edit mode because it was unable to update all the properties of the "category".
If I change TryUpdateModel to UpdateModel it throws an error saying "the model of type WR_TakeAway_Menu_Categories could not be updated"
Is there a better way of doing this, or some way to allow TryUpdateModel to return true to allow the grid to return to display mode?
Without seeing your WR_TakeAway_Menu_Categories class, I'm going to assume that you have some other classes as properties of your WR_TakeAway_Menu_Categories class.
If that is the case, you'll need to exclude the custom objects from the TryUpdateModel method and set those manually before hand.
For example:
db.Entry(category).Reference(c => c.CreatedByUser).CurrentValue = CreatedByUser;
db.Entry(category).Reference(c => c.LastUpdateByUser).CurrentValue = LastUpdateByUser;
This will set your "custom object" variables to the latest value. I have noticed that in some cases if you do not do it this way, and instead just set the property explicitly, the database record will not always get updated.
After you have manually updated the custom properties, then call the TryUpdateModel, excluding the properties that you set manually.
TryUpdateModel<WR_TakeAway_Menu_Categories>(category, null, null, new[] { "CreatedByUser", "LastUpdateByUser" });

Partial Record Updates with Linq to SQL and MVC

Let's say I have a DB table with columns A and B and I've used the Visual Studio designer to create a Linq objects for this table. All fields are marked NOT NULL.
Now I'm trying to edit this record using typical MVC form editing and model binding, but field B doesn't need to be editable, so I don't include it in the form.
When the post handler binds the object it doesn't populate field B, leaving it null. Now when I save the object I get a DB error saying field B can't be NULL.
The code to save looks something like:
m_DataContext.MyObjects.Attach(myObject);
m_DataContext.Refresh(RefreshMode.KeepCurrentValues, myObject);
m_DataContext.SubmitChanges();
How do I get this to work? Do I need to include field B as a hidden field on the form - I don't really want to do this as it may be updated by other users at the same time and I don't want to stomp over it.
I've found the solution to this problem revolves around getting the entity object associated with the data context before applying the changes. There's a couple of ways of doing this which I've described in separate answers below.
Descend into SQL
This approach ditches LINQ in favour of straight SQL:
public override void SaveMyObject(MyObject o)
{
// Submit
m_DataContext.ExecuteCommand("UPDATE MyObjects SET A={0} WHERE ID={1}", o.ID, o.A);
}
I like this approach the best because of it's simplicity. As much as I like LINQ I just can't justify it's messiness with this problem.
Use a Custom Model Binder
This approach uses a custom model binder to create the entity object and associate with the data context, before the binding takes place.
public class MyObjectBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
MyObject a = ((MyObjectController)controllerContext.Controller).Repository.GetMyObjectForUpdate(bindingContext.ValueProvider["ID"].AttemptedValue.ToString());
return a;
}
}
The repository then creates the object and associates it with the data context:
public Object GetMyObjectForUpdate(string id)
{
MyObject o=new MyObject();
o.ID=id;
m_DataContext.Articles.Attach(o);
m_DataContext.Refresh(RefreshMode.KeepCurrentValues);
return o;
}
The action handler needs to be attributed to use the model binder...
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult EditMyObject([ModelBinder(typeof(MyObjectBinder))] MyObject o)
{
if (!ModelState.IsValid)
return View("EditMyObject", a);
Repository.SaveMyObject(a);
return RedirectToAction("Index");
}
and finally SaveMyObject simply calls datacontext.SubmitChanges().
For this to work I also needed to set the update check attributes on all columns to Never (in the dbml file).
Overall, this approach works but is messy.
Use Two Entity Objects
This approach uses two entity objects, one for the model binding and one LINQ:
public override void SaveMyObject(MyObject o)
{
// Create a second object for use with linq and attach to the data context
MyObject o2 = new MyObject();
o2.ID = o.ID;
m_DataContext.Articles.Attach(o2);
m_DataContext.Refresh(RefreshMode.KeepCurrentValues);
// Apply fields edited by the form
o2.A = o.A;
// Submit
m_DataContext.SubmitChanges();
}
This approeach doesn't require any special handling in the controller (ie: no custom model binding) but still requires
the Update Check property to be set to Never in the dbml file.
You could add a timestamp field and check one on the page with the one in the DB (hiding the timestamp field as well). If a user has updated the record, a concurrency error is returned and the page is refreshed, or left the same iwth the users changes.

Resources