【问题标题】:MVC Model Binding a Complex Type to a Simple Type and Vice VersaMVC 模型将复杂类型绑定到简单类型,反之亦然
【发布时间】:2011-02-08 06:55:04
【问题描述】:

这是一个场景:

我有一个自动完成插件(自定义),它保留 JSON 对象的隐藏字段(使用特定结构)。

我创建了一个 Html 帮助程序,可以帮助我轻松绑定到特定的自定义模型(基本上,它有一个用于双向绑定的 JSON 属性和一个让我将 JSON 反序列化为适当结构的属性):

public class AutoCompleteModel {
    public string JSON { get; set; }
    public IEnumerable<Person> People {
        get {
            return new JavaScriptSerializer().Deserialize<Person>(this.JSON);
        }
        set {
            this.JSON = new JavaScriptSerializer().Serialize(value);
        }
     }
 }

这很好用,我可以使用默认绑定器@Html.Autocomplete(viewModel =&gt; viewModel.AutoCompleteModelTest) 对绑定进行建模。 HTML 助手生成 HTML 如下:

<input type="text" id="AutoCompleteModelTest_ac" name="AutoCompleteModelTest_ac" value="" />
<input type="hidden" id="AutoCompleteModelTest_JSON" name="AutoCompleteModelTest.JSON" value="{JSON}" />

问题是这对消费者来说不是最好的方式。他们必须手动将 People 属性设置为 Person 结构数组。在我的数据层中,我的域对象可能不会存储完整的结构,只有人的 ID(公司 ID)。如果只给出一个 ID,自动完成功能将负责查找此人本身。

最好的情况是这样称呼它:

@Html.Autocomplete(domainObject =&gt; domainObject.PersonID)@Html.Autocomplete(domainObject =&gt; domainObject.ListOfPersonIDs

我希望它对字符串属性和自定义 AutoCompleteModel 起作用。自动完成器仅更新单个隐藏字段,并且该字段名称在回发时传回(值类似于:[{ "Id":"12345", "FullName":"A Name"},{ "Id":"12347", "FullName":"Another Name" }])。

当然,问题在于这些域对象属性只有一个 ID 或 ID 数组,而不是完整的 Person 结构(因此不能直接序列化为 JSON)。在 HTML 帮助器中,我可以将这些属性值转换为结构,但我不知道如何在 POST 上将其转换回简单类型。我需要的解决方案是在页面加载时将 ID 转换为新的 Person 结构,将其序列化到隐藏字段中。在 POST 上,它会将生成的 JSON 反序列化回一个简单的 ID 数组。

自定义模型绑定器是我需要的解决方案吗?我如何告诉它同时使用自定义模型和简单类型(因为我不希望它应用于每个字符串属性,只需要它来处理 HTML 助手给出的值)。

【问题讨论】:

    标签: model-view-controller model-binding


    【解决方案1】:

    我想通了,有可能!

    为了澄清,我需要:将字符串或字符串数​​组(ID)转换为我的隐藏字段值的 JSON 结构,然后在回发时,反序列化隐藏字段中的 JSON 并将结构转换回简单的我的域对象属性的字符串或字符串数​​组(ID)。

    第 1 步:创建 HTML 帮助程序

    我已经这样做了,但只是为了接受我的自定义 AutoCompleteModel 类型。我需要一个字符串和一个字符串类型的 Enumerable。

    我所做的只是从属性的值生成我的 Person 结构并将它们序列化为 JSON 用于自动完成器使用的隐藏字段(这是 string 帮助器的示例,我也有一个几乎相同的一个IEnumerable&lt;string&gt;):

    public static MvcHtmlString AutoComplete<TModel>(
        this HtmlHelper<TModel> htmlHelper,
        Expression<Func<TModel, string>> idProp)
        where TModel : class
    {
        TModel model = htmlHelper.ViewData.Model;
        string id = idProp.Compile().Invoke(model);
    
        string propertyName = idProp.GetPropertyName();
    
        Person[] people = new Person[] {
            new Person() { ID = id }
        };
    
        // Don't name the textbox the same name as the property,
        // otherwise the value will be whatever the textbox is,
        // if you care.
        MvcHtmlString textBox = htmlHelper.TextBox(propertyName + "_ac", string.Empty);
    
        // For me, the JSON is the value I want to postback
        MvcHtmlString hidden = htmlHelper.Hidden(propertyName, new JavaScriptSerializer().Serialize(people));
    
        return MvcHtmlString.Create(
            "<span class=\"AutoComplete\">" +
                textBox.ToHtmlString() +
                hidden.ToHtmlString() +
            "</span>");
    }
    

    用法:@Html.AutoComplete(model =&gt; model.ID)

    第 2 步:创建自定义模型绑定器

    我的问题的症结在于我需要这个活页夹只适用于某些属性,它们是字符串或字符串数​​组。

    我受到这个article 的启发,因为它使用了泛型。我决定,嘿,我们可以问人们他们想为什么属性应用活页夹。

    public class AutoCompleteBinder<T> : DefaultModelBinder
        where T : class
    {
        private IEnumerable<string> PropertyNames { get; set; }
    
        public AutoCompleteBinder(params Expression<Func<T, object>>[] idProperties)
        {
            this.PropertyNames = idProperties.Select(x => x.GetPropertyName());
        }
    
        protected override object GetPropertyValue(
            ControllerContext controllerContext, 
            ModelBindingContext bindingContext,
            PropertyDescriptor propertyDescriptor, 
            IModelBinder propertyBinder)
        {
            var submittedValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
    
            if (submittedValue != null && this.PropertyNames.Contains(propertyDescriptor.Name))
            {
                string json = submittedValue.AttemptedValue;
    
                Person[] people = new JavaScriptSerializer().Deserialize<Person[]>(json);
    
                if (people != null && people.Any())
                {
                    string[] IDs = people.Where(x => !string.IsNullOrEmpty(x.ID)).Select(x => x.ID).ToArray();
    
                    bool isArray = bindingContext.ModelType != typeof(string) && 
                        (bindingContext.ModelType == typeof(string[]) || 
                        bindingContext.ModelType.HasInterface<IEnumerable>());
    
                    if (IDs.Count() == 1 && !isArray)
                        return IDs.First(); // return string
                    else if (IDs.Count() > 0 && isArray)
                        return IDs.ToArray(); // return string[]
                    else
                        return null;
                }
                else
                {
                    return null;
                }
            }
    
            return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
        }
    }
    

    GetPropertyName()(将 LINQ 表达式转换为字符串,即m =&gt; m.ID = ID)和HasInterface() 只是我拥有的两个实用方法。

    第三步:注册

    Application_Start 中注册您的域对象及其属性的活页夹:

    ModelBinders.Binders.Add(typeof(Employee), new AutoCompleteBinder<Employee>(e => e.ID, e => e.TeamIDs));
    

    必须为特定属性注册活页夹只是有点烦人,但这不是世界末日,使用我的自动完成程序提供了良好、流畅的体验。

    欢迎任何cmets。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-05-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-03-26
      相关资源
      最近更新 更多