可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I have a model that contains a dictionary property. (this has been distilled from a larger project into this example, which I have confirmed still has the same issue)
public class TestModel
{
public IDictionary<string, string> Values { get; set; }
public TestModel()
{
Values = new Dictionary<string, string>();
}
}
a controller
public class TestController : Controller
{
public ActionResult Index()
{
TestModel model = new TestModel();
model.Values.Add("foo", "bar");
model.Values.Add("fizz", "buzz");
model.Values.Add("hello", "world");
return View(model);
}
[HttpPost]
public ActionResult Index(TestModel model)
{
// model.Values is null after post back here.
return null; // I set a break point here to inspect 'model'
}
}
and a view
@using TestMVC.Models
@model TestModel
@using (Html.BeginForm())
{
@Html.EditorFor(m => m.Values["foo"]);
<br />
@Html.EditorFor(m => m.Values["fizz"]);
<br />
@Html.EditorFor(m => m.Values["hello"]);
<br />
<input type="submit" value="submit" />
}
This renders to the browser like this:
<input class="text-box single-line" id="Values_foo_" name="Values[foo]" type="text" value="bar" />
The problem I'm having is that the dictionary is null on the model after postback.
- Am I doing this right, or is there a better way?
I need to have some kind of key-value storage, as the fields on my form are variable, so I can't use a POCO model.
回答1:
Read through Scott hanselman's blog post on the topic for more details but in the mean time,
in order to fix your issue, just replace your view as the following :
<input type="hidden" name="Values[0].Key" value="foo" />
<input type="text" name="Values[0].Value" value="bar" />
Repeat the same for all sections , maybe put it in a for loop like :
@for(i=0;i<Model.Values.Count;i++)
{
@Html.Hidden("Values[@i].Key", @Model.Values.Keys[@i])
@Html.TextBox("Values[@i].Value", @Model.Values.Values[@i])
}
Note that you can access Keys and Values through an index only if you use an OrderedDictionary
回答2:
Scott hanselman shows how to do ModelBinding to Dictionary
http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx
Quote from the blog
If the signature looks like this:
public ActionResult Blah(IDictionary<string, Company> stocks) {
// ...
}
And we are given this in HTML:
<input type="text" name="stocks[0].Key" value="MSFT" />
<input type="text" name="stocks[0].Value.CompanyName" value="Microsoft Corporation" />
<input type="text" name="stocks[0].Value.Industry" value="Computer Software" />
<input type="text" name="stocks[1].Key" value="AAPL" />
<input type="text" name="stocks[1].Value.CompanyName" value="Apple, Inc." />
<input type="text" name="stocks[1].Value.Industry" value="Consumer Devices" />
http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx
@model Dictionary<string, string>
@for (int i = 0; i < 3; i++)
{
Html.EditorFor(m => m[i].Value)
{
I Think It would also work by key as well such as
Html.EditorFor(m => m.Values["foo"].Value)
回答3:
If you need to bind a Dictionary, such that each value has a texbox to edit it, below is one way to make it work. The really important parts which effect how the name attribute in the HTML is generated is the model expression, which is what ensures the model binding occurs on postback. This example only works for Dictionary.
The linked article explains the HTML syntax that makes the binding work, but it leaves the Razor syntax to accomplish this quite a mystery. Also, the article is quite different in that they are allowing both Keys and Values to be edited, and are using an integer index even though the dictionary’s key is a string, not an integer. So if you are trying to bind a Dictionary, you’ll really need to evaluate first whether you just want Values to be editable, or both keys and values, before you decide on which approach to take, because those scenarios are quite different.
If you ever need to bind to a complex object, i.e. Dictionary then you should just be able to have a textbox for each property with the expression drilling into the property, similar to the article.
http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx
public class SomeVM
{
public Dictionary<string, string> Fields { get; set; }
}
public class HomeController : Controller
{
[HttpGet]
public ViewResult Edit()
{
SomeVM vm = new SomeVM
{
Fields = new Dictionary<string, string>() {
{ "Name1", "Value1"},
{ "Name2", "Value2"}
}
};
return View(vm);
}
[HttpPost]
public ViewResult Edit(SomeVM vm) //Posted values in vm.Fields
{
return View();
}
}
CSHTML:
Editors for Values only(of course you could add LabelFor to generate labels based on the Key):
@model MvcApplication2.Controllers.SomeVM
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
<legend>SomeVM</legend>
@foreach(var kvpair in Model.Fields)
{
@Html.EditorFor(m => m.Fields[kvpair.Key]) //html: <input name="Fields[Name1]" …this is how the model binder knows during the post that this textbox value gets stuffed in a dictionary named “Fields”, either a parameter named Fields or a property of a parameter(in this example vm.Fields).
}
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
Editing both Keys/Values:
@{ var fields = Model.Fields.ToList(); }
@for (int i = 0; i < fields.Count; ++i)
{
//It is important that the variable is named fields, to match the property name in the Post method's viewmodel.
@Html.TextBoxFor(m => fields[i].Key)
@Html.TextBoxFor(m => fields[i].Value)
//generates using integers, even though the dictionary doesn't use integer keys,
//it allows model binder to correlate the textbox for the key with the value textbox:
//<input name="fields[0].Key" ...
//<input name="fields[0].Value" ...
//You could even use javascript to allow user to add additional pairs on the fly, so long as the [0] index is incremented properly
}
回答4:
As @Blast_Dan and @gprasant mentioned, the model binder is expecting the name attribute of the input element to be in the format Property[index].Value
, where index
is an int
and Value
is one of the properties on the KeyValuePair
class.
Unfortunately, @Html.EditorFor
generates this value in the wrong format. I wrote an HtmlHelper extension to transform the name attribute to the correct format:
public static IHtmlString DictionaryEditorFor<TModel, TProperty, TKey, TValue>(this HtmlHelper<TModel> Html, Expression<Func<TModel, TProperty>> expression, IDictionary<TKey, TValue> dictionary, DictionaryIndexRetrievalCounter<TKey, TValue> counter, string templateName, object additionalViewData)
{
string hiddenKey = Html.HiddenFor(expression).ToHtmlString();
string editorValue = Html.EditorFor(expression, templateName, additionalViewData).ToHtmlString();
string expText = ExpressionHelper.GetExpressionText(expression);
string indexText = expText.Substring(expText.IndexOf('[')).Replace("[", string.Empty).Replace("]", string.Empty);
KeyValuePair<TKey, TValue> item = dictionary.SingleOrDefault(p => p.Key.ToString() == indexText);
int index = counter.GetIndex(item.Key);
string key = hiddenKey.Replace("[" + indexText + "]", "[" + index + "].Key").Replace("value=\"" + item.Value + "\"", "value=\"" + item.Key + "\"");
string value = editorValue.Replace("[" + indexText + "]", "[" + index + "].Value");
return new HtmlString(key + value);
}
Because the integer index must follow these rules:
Must start with 0
Must be unbroken (you can't skip from 3 to 5, for example)
I wrote a counter class to handle getting the integer index for me:
public class DictionaryIndexRetrievalCounter<TKey, TValue>
{
private IDictionary<TKey, TValue> _dictionary;
private IList<TKey> _retrievedKeys;
public DictionaryIndexRetrievalCounter(IDictionary<TKey, TValue> dictionary)
{
this._dictionary = dictionary;
this._retrievedKeys = new List<TKey>();
}
public int GetIndex(TKey key)
{
if (!_retrievedKeys.Contains(key))
{
_retrievedKeys.Add(key);
}
return _retrievedKeys.IndexOf(key);
}
}