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

发布于 2024-10-07 09:14:18 字数 899 浏览 0 评论 0原文

在我的 ASP.NET MVC 2 Web 应用程序中,我允许用户创建不同数据类型的自定义输入字段来扩展我们的基本输入表单。虽然很棘手,但从自定义字段集合构建输入表单已经足够简单了。

但是,我现在想要处理此表单的发布,但我不确定处理此问题的最佳方法是什么。通常,我们会使用强类型输入模型,该模型从表单上可用的各种静态类型输入绑定。但是,我不知道如何使用代表不同数据类型的可变数量的输入字段来执行此操作。

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

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

我考虑过的想法是:

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

以前有人解决过这样的问题吗?如果是这样,你是如何解决的?

更新:

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

更新2:

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

In my ASP.NET MVC 2 web application, I allow users to create custom input fields of different data types to extend our basic input form. While tricky, building the input form from a collection of custom fields is straight-forward enough.

However, I'm now to the point where I want to handle the posting of this form and I'm not certain what the best way to handle this would be. Normally, we'd use strongly-typed input models that get bound from the various statically-typed inputs available on the form. However, I'm at a loss for how to do this with a variable number of input fields that represent different data types.

A representative input form might look something like:

  • My date field: [ date time input
    control ]
  • My text field: [ text input
    field ]
  • My file field: [ file upload
    control ]
  • My number field: [ numerical input control ]
  • My text field 2: [text input field ]
  • etc...

Ideas I've thought about are:

  • Sending everything as strings (except for the file inputs, which would need to be handled specially).
  • Using a model with an "object" property and attempting to bind to that (if this is even possible).
  • Sending a json request to my controller with the data encoded properly and attempting to parse that.
  • Manually processing the form collection in my controller post action - certainly an option, but I'd love to avoid this.

Has anyone tackled an issue like this before? If so, how did you solve it?

Update:

My "base" form is handled on another input area all together, so a solution doesn't need to account for any sort of inheritence magic for this. I'm just interested in handling the custom fields on this interface, not my "base" ones.

Update 2:

Thank you to ARM and smartcaveman; both of you provided good guidance for how this could be done. I will update this question with my final solution once its been implemented.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

对你的占有欲 2024-10-14 09:14:18

这就是我开始处理这个问题的方式。基于 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。它对我构建自定义模型绑定器非常有帮助。

更新

我意识到我误解了这个问题,并且他专门询问如何处理“具有代表不同数据类型的可变数量的输入字段”的表单的发布。我认为最好的方法是使用与上面类似的结构,但利用 复合模式。基本上,您需要创建一个类似 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 来访问自定义表单。有了这个假设,模型绑定器和提供者可能看起来像这样。

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 构建强类型代理并不是一个巨大的步骤,但由于您的用户在网站上动态创建表单,因此您的解决方案中可能没有强类型模型,并且这是无关紧要的)。希望这有更多帮助。

This is how I would begin to approach the issue. A custom model binder would be pretty easy to build based on the FormKey property (which could be determined by the index and/or label, depending).

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; }
}

Also, I noticed one of the comments below had a filtered model binder system. Jimmy Bogard from Automapper made a really helpful post about this method at http://www.lostechies.com/blogs/jimmy_bogard/archive/2009/03/17/a-better-model-binder.aspx , and later revised in, http://www.lostechies.com/blogs/jimmy_bogard/archive/2009/11/19/a-better-model-binder-addendum.aspx . It has been very helpful for me in building custom model binders.

Update

I realized that I misinterpreted the question, and that he was specifically asking how to handle posting of the form "with a variable number of input fields that represent different data types". I think the best way to do this is to use a structure similar to above but leverage the Composite Pattern. Basically, you will need to create an interface like IFormComponent and implement it for each datatype that would be represented. I wrote and commented an example interface to help explain how this would be accomplished:

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);
} 

I am assuming that the custom forms can be accessed via some sort of id which can be contained as a form parameter. With that assumption, the model binder and provider could look something like this.

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.");
    }
}

Obviously, all the code here is just to get the point across, and would need to be completed and cleaned up for actual use. Also, even if completed this would only bind to an implementation of the IForm interface, not a strongly typed business object. (It wouldn't be a huge step to convert it to a dictionary and build a strongly typed proxy using the Castle DictionaryAdapter, but since your users are dynamically creating the forms on the site, there is probably no strongly typed model in your solution and this is irrelevant). Hope this helps more.

救赎№ 2024-10-14 09:14:18

看看我在这里做了什么:处理多个模型的 MVC2 Action< /a> 看看是否能让你走上正轨。

如果您使用 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.
    }
}

Take a peek at what I did here: MVC2 Action to handle multiple models and see if can get you on the right track.

If you use a FormCollection as one of your parameters to your action, you can then go thru that form collection looking for bits of data here or there in order to bind those values to whatever an then save the data. You are most likely going to need to take advantage of both strategy and command patterns to get this to work.

Best of luck, feel free to ask follow-up questions.

Edit:

Your method which does the work should look something like this:

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.
    }
}
零崎曲识 2024-10-14 09:14:18

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

也许这些文章可以帮助您:

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

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

/胜利者

I would think one of the best options is to create a custom model binder, which makes it possible to have custom logic behind the scenes and still very customizable code behind.

Maybe these articles can help you:

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

More specifically I would probably take as the controller argument a custom class with all "base" properties included. The class could then for example include a dictionary linking the name of each field to either just an object or an interface which you implement once for each data-type making it simple to process the data later.

/Victor

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文