Question

In ASP.NET MVC4, I want to initialize fields with values. When the user gets the page, before he has posted it back, I want to have it start out with values in some fields. The values will be drawn from the query string, not hard coded. Say a user is filling out a form: He's logged in, you know his name and address. As a courtesy, make those the default values. Doesn't matter where they come from, really, except that it's a set of key-value pairs, and the values are strings. I just want to put something in fields without the user posting the form first and I'd really like to do it without hard-coding a long, long list of assignments to every property in a rather complicated model.

Right now it's done in a JS loop in $(document).ready(), but it belongs on the server. I'd like to replicate that logic, though: Treat the query param names as unique identifiers.

In the Index() method of my controller, I tried calling ModelState.TrySetModelValue() (which when ModelState is populated, identifies each field by one unique string) but at this stage, ModelState is empty, so of course that didn't work. I tried changing Index() to expect an instance of the model as a parameter, but that doesn't help.

Must I rewrite every @Html.EditorFor()/TextBoxFor()/etc. call in the application? That seems crazy. Properly, this is something I'd do in a loop, in one place, not scattered around in multiple spots in each of a growing number of views.

I have a feeling that I'm failing to grasp something fundamental about the way MVC4 is intended to work.

UPDATE 2

It turns out that if you decorate your action method with [HttpGet], and you have it expect the model as a parameter, then if you use the field names (foo.bar) rather than IDs (foo_bar) in the query string, it does what I want automatically. ModelState is populated. I must not have had the action method decorated with [HttpGet] when I looked at ModelState.

If a field is set via query string automatically, that supersedes whatever's in your model. That's reasonable; the whole point is to override the model's default values. But if you want to in turn override possible query string values (e.g., say there's a checkbox for an "electronic signature"; that should always require an explicit effort on the user's part), then you've got to do that via ModelState.

That means that my first solution, below, had no actual effect (provided I had the [HttpGet] property on the action method). It only set properties of the model which had already been set in ModelState by the framework, and whose values in the model were therefore ignored.

What's a little bit stranger is that ModelState gives fields a different key if they're not in the query string. foo.bar.baz uses just that as a key if it's in the query string, but if it isn't, the key becomes foo.footypename.bar.bartypename.baz. There appears to be an exception if the property's name is the same as it's type: I have a Name model class, and another model class has a property public Name Name { get; set }. Properties of type Name, which are named name, are never followed by their type name in the ModelState keys. However, I have not yet ruled out other possible reasons for that particular property having its typename excluded. That's a guess. The typenames are excluded for "leaf" properties in all cases in my model. Is that because they're types known to the system, or "leaves", or what? I don't know.

In any case, a leaf property of the "root" class of the model always uses its own name as a key in ModelState.

So the generalized answer is you assign to the model. But there's a different specific answer for initialization from a query string.

UPDATE

Solution -- much code snipped

//  Controller base
public abstract class ControllerBase<TModel> : Controller 
{
    [HttpGet]
    public virtual ActionResult Index(TModel model)
    {
        HttpContext.Request.QueryString.CopyTo(model);

        return View("Index", model);
    }
}

public static class Extensions
{
    /// <summary>
    /// Given NameValueCollection of keys/values in the form 
    /// "foo.bar.baz" = "text", and an object which is the *parent* of
    /// foo, set properties of foo accordingly. 
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="src"></param>
    /// <param name="model"></param>
    public static void CopyTo<T>(this NameValueCollection src, T target)
    {
        String strkey;
        Object objval;

        foreach (var key in src.Keys)
        {
            strkey = "" + key;
            objval = src[strkey];

            target.TrySetPropertyValue(strkey, objval);
        }
    }

    /// <summary>
    /// Given a reference to an object objThis, the string "foo.bar.baz", 
    /// and an object o of a type optimistically hoped to be convertible 
    /// to that of objThis.foo.bar.baz, set objThis.foo.bar.baz = o
    ///
    /// If foo.bar is null, it must have a default constructor, or we fail 
    /// and return false. 
    /// </summary>
    /// <param name="objThis"></param>
    /// <param name="propPathName"></param>
    /// <param name="value"></param>
    /// <returns></returns>
    public static bool TrySetPropertyValue(this object objThis, 
                                           string propPathName, object value)
    {
        if (string.IsNullOrWhiteSpace(propPathName))
        {
            throw new ArgumentNullException(propPathName);
        }
        var names = propPathName.Split(new char[] { '.' }).ToList();

        var nextPropInfo = objThis.GetType().GetProperty(names.First());

        if (null == nextPropInfo)
            return false;

        if (names.Count > 1)
        {
            var nextPropValue = nextPropInfo.GetValue(objThis, null);

            if (null == nextPropValue)
            {
                nextPropValue = Activator
                    .CreateInstance(nextPropInfo.PropertyType);
                nextPropInfo.SetValue(objThis, nextPropValue);
            }

            names.RemoveAt(0);

            return nextPropValue.TrySetPropertyValue(
                                     String.Join(".", names), value);
        }
        else
        {
            try
            {
                var conv = System.ComponentModel.TypeDescriptor
                               .GetConverter(nextPropInfo.PropertyType);
                value = conv.ConvertFrom(value);

                nextPropInfo.SetValue(objThis, value);
            }
            catch (System.FormatException)
            {
                return false;
            }
            return true;
        }
    }
}
Était-ce utile?

La solution

You can initialize your model in controller with default values and then use it like

@Html.TextBoxFor(m => Model.Name)

Initialization in Controller:

public ActionResult Index()
{
   MyModel model = new MyModel();
   model.Name = "myname";
   return View("myview", model);
}

You can also set the attributes in TextBoxFor

@Html.TextBoxFor(m => Model.Name, new { value = "myname"})

Update

If your url looks like mysite/Edit?id=123 try decalring your controller action like

public ActionResult Edit(string id) 
{ ... 

Also try decorating it with HttpPost or HttpGet attribute

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top