ASP.NET MVC - 发布包含不同数据类型的自定义字段的表单

时间:2010-12-10 18:55:02

标签: asp.net asp.net-mvc viewmodel

在我的ASP.NET MVC 2 Web应用程序中,我允许用户创建不同数据类型的自定义输入字段以扩展我们的基本输入表单。虽然很棘手,但是从一组自定义字段构建输入表单非常简单。

但是,我现在要处理这个表单的发布,我不确定处理这个问题的最佳方法是什么。通常,我们使用强类型输入模型,它们受到表单上可用的各种静态类型输入的约束。但是,对于如何使用表示不同数据类型的可变数量的输入字段,我感到很茫然。

代表性输入表单可能类似于:

  • 我的日期字段:[日期时间输入 控制]
  • 我的文字字段:[文字输入 字段]
  • 我的文件字段:[文件上传 控制]
  • 我的号码字段:[数字输入控制]
  • 我的文字字段2:[文字输入字段]
  • 等...

我想过的想法是:

  • 将所有内容作为字符串发送(文件输入除外,需要特别处理)。
  • 使用具有“object”属性的模型并尝试绑定到该属性(如果可能的话)。
  • 将json请求发送到我的控制器,并正确编码数据并尝试解析该数据。
  • 在我的控制器发布操作中手动处理表单集合 - 当然是一种选择,但我很想避免这种情况。

有没有人之前处理过这样的问题?如果是这样,你是如何解决的?

更新

我的“基础”表单一起处理在另一个输入区域,因此解决方案不需要为此考虑任何类型的继承魔法。我只对处理这个界面上的自定义字段感兴趣,而不是我的“基础”字段。

更新2:

感谢ARM和smartcaveman;你们俩都为如何做到这一点提供了很好的指导。一旦实施,我将用我的最终解决方案更新这个问题。

3 个答案:

答案 0 :(得分:1)

看一看我在这里所做的事情:MVC2 Action to handle multiple models,看看能不能让你走上正轨。

如果您使用FormCollection作为操作的参数之一,则可以通过该表单集合在此处或那里查找数据位,以便将这些值绑定到任何然后保存数据。您很可能需要利用策略和命令模式来实现这一点。

祝你好运,随时提出后续问题。

编辑:

你做这项工作的方法应该是这样的:

private/public void SaveCustomFields(var formId, FormCollection collection) //var as I don't know what type you are using to Id the form.
{
    var binders = this.binders.select(b => b.CanHandle(collection)); //I used IOC to get my list of IBinder objects
    // Method 1:    
    binders.ForEach(b => b.Save(formId, collection)); //This is the execution implementation.
    // Method 2:
    var commands = binders.Select(b => b.Command(formId, collection));
    commands.ForEach(c => c.Execute());    
}

public DateBinder : IBinder //Example binder
{
    public bool CanHandle(FormCollection collection)
    {
        return (null != collection["MyDateField"]); //Whatever the name of this field is.
    }

    //Method 1
    public void Save(var formId, FormCollection collection)
    {
        var value = DateTime.Parse(collection["MyDateField"]);
        this.someLogic.Save(formId, value); //Save the value with the formId, or however you wish to save it.
    }
    //Method 2
    public Command Command(var formId, FormCollection collection)
    {
        //I haven't done command pattern before so I'm not sure exactly what to do here.
        //Sorry that I can't help further than that.
    }
}

答案 1 :(得分:1)

这就是我开始解决这个问题的方法。基于FormKey属性(可以由索引和/或标签确定,取决于)可以很容易地构建自定义模型绑定器。

public class CustomFormModel
{
    public string FormId { get; set; }
    public string Label { get; set; }
    public CustomFieldModel[] Fields { get; set; }
}
public class CustomFieldModel
{
    public DataType DateType { get; set; } //  System.ComponentModel.DataAnnotations
    public string FormKey { get; set; }
    public string Label { get; set; }
    public object Value { get; set; }
}
public class CustomFieldModel<T> : CustomFieldModel
{
    public new T Value { get; set; }
}

另外,我注意到下面的评论之一有一个过滤的模型绑定系统。来自Automapper的Jimmy Bogard在http://www.lostechies.com/blogs/jimmy_bogard/archive/2009/03/17/a-better-model-binder.aspx发表了关于此方法的非常有用的帖子,后来在http://www.lostechies.com/blogs/jimmy_bogard/archive/2009/11/19/a-better-model-binder-addendum.aspx进行了修订。它对我构建自定义模型绑定器非常有帮助。

<强>更新

我意识到我误解了这个问题,并且他特意询问如何处理“使用表示不同数据类型的可变数量的输入字段”的表单的发布。我认为最好的方法是使用类似于上面的结构,但利用Composite Pattern。基本上,您需要创建一个类似IFormComponent的接口,并为将要表示的每种数据类型实现它。我编写并评论了一个示例界面,以帮助解释如何实现这一目标:

public interface IFormComponent
{
    //  the id on the html form field.  In the case of a composite Id, that doesn't have a corresponding 
    //  field you should still use something consistent, since it will be helpful for model binding
    //  (For example, a CompositeDateField appearing as the third field in the form should have an id 
    //  something like "frmId_3_date" and its child fields would be "frmId_3_date_day", "frmId_3_date_month",
    //  and "frmId_3_date_year".
    string FieldId { get; }

    //  the human readable field label
    string Label { get; }

    //  some functionality may require knowledge of the 
    //  Parent component.  For example, a DayField with a value of "30"
    //  would need to ask its Parent, a CompositeDateField
    //  for its MonthField's value in order to validate
    //  that the month is not "February"
    IFormComponent Parent { get; }

    //  Gets any child components or null if the 
    //  component is a leaf component (has no children).
    IList<IFormComponent> GetChildren();

    //  For leaf components, this method should accept the AttemptedValue from the value provider
    //  during Model Binding, and create the appropriate value.  
    //  For composites, the input should be delimited in someway, and this method should parse the 
    //  string to create the child components.  
    void BindTo(string value);

    //  This method should parse the Children or Underlying value to the 
    //  default used by your business models.  (e.g. a CompositeDateField would 
    //  return a DateTime.  You can get type safety by creating a FormComponent<TValue>
    //  which would help to avoid issues in binding.
    object GetValue();

    //  This method would render the field to the http response stream.
    //  This makes it easy to render the forms simply by looping through 
    //  the array.  Implementations could extend this for using an injected 
    //  formatting 
    void Render(TextWriter writer);
} 

我假设可以通过某种id访问自定义表单,这些id可以作为表单参数包含。有了这个假设,模型绑定器和提供者可能看起来像这样。

public interface IForm : IFormComponent
{
    Guid FormId { get; }
    void Add(IFormComponent component);
}
public interface IFormRepository
{
    IForm GetForm(Guid id);
}
public class CustomFormModelBinder : IModelBinder   
{
    private readonly IFormRepository _repository;
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        ValueProviderResult result;
        if(bindingContext.ValueProvider.TryGetValue("_customFormId", out result))
        {
            var form = _repository.GetForm(new Guid(result.AttemptedValue));
            var fields = form.GetChildren();
            //  loop through the fields and bind their values 
            return form;
        }
        throw new Exception("Form ID not found.");
    }
}

显然,这里的所有代码只是为了得到重点,并且需要完成并清理以供实际使用。此外,即使已完成,这只会绑定到IForm接口的实现,而不是强类型的业务对象。 (将它转换为字典并使用Castle DictionaryAdapter构建强类型代理不是一个巨大的步骤,但由于您的用户在网站上动态创建表单,因此您的解决方案中可能没有强类型模型这是无关紧要的)。希望这会有所帮助。

答案 2 :(得分:0)

我认为最好的选择之一是创建一个自定义模型绑定器,这样就可以在幕后拥有自定义逻辑,并且仍然可以定制代码。

也许这些文章可以帮到你:

http://www.gregshackles.com/2010/03/templated-helpers-and-custom-model-binders-in-asp-net-mvc-2/

http://www.singingeels.com/Articles/Model_Binders_in_ASPNET_MVC.aspx

更具体地说,我可能会将控制器参数作为包含所有“基本”属性的自定义类。然后,该类可以包括一个字典,将每个字段的名称链接到一个对象或一个接口,您可以为每个数据类型实现一次,以便以后处理数据变得简单。

/维克多