提交由部分(异步)视图组成的表单

本文关键字:视图 表单 异步 提交 | 更新日期: 2023-09-27 18:33:23

我试图让JQuery-Tabcontrol与异步Tabs一起工作。

该选项卡控件是表单的一部分,问题是,提交并不能真正与视图模型的异步加载部分一起使用。

我的视图模型类似于窗口窗体。它由带有子项的控件组成。每个控件都有一个编辑器模板。我试图让我的选项卡控件异步工作。(它同步工作得很好(但这部分让我从周五开始就很忙。

使用这些编辑器模板,我需要编写一个自定义模型绑定器。当我在其中放置断点时,对于按需加载的视图部分,它不会命中。但是,当我深入研究控制器的值提供程序/表单集合时,我发现了这些值。因此,它肯定会接收所有数据,但它不想为此调用 Binders。

如何让它调用绑定器,以便输入到其中一个选项卡中的数据绑定到视图模型?

这是选项卡控件的视图

@model {..}.TabControlViewModel
<div class="tabs">
  <ul>
    @for (int i = 0; i < Model.Pages.Count; i++)
    {
      <li>
          @Html.ActionLink(Model.Pages[i].Title, "ShowPartial", new  { formId = Model.FormId, controlId = Model.Pages[i].Id})
      </li>
    }
  </ul>
</div>

这是控制器方法:

    public async Task<ActionResult> ShowPartial(int formId, int controlId)
    {
        TabPageViewModel tabPage= await FormControlManager.GetTabPage(this, formId, controlId);
        await Task.Delay(1000); // just for testing
        return PartialView(tabPage);
    }

显示部分视图

@model {...}.ControlViewModel
@Html.EditorFor(x => x)

一些更详细的信息

整个 Control-Nesting-Idea 基于本教程:

http://blogs.microsoft.co.il/gilf/2012/04/23/generating-aspnet-mvc-view-controls-according-to-xml-configurations/(虽然它不是平面的,而是递归的(如窗口窗体((

所以基本上一切都是FormControl的成员。它也由控制器的初始操作返回,称为"显示">

当用户选择一个选项卡时,它通过传递 formid 和 controlId 来获取窗体的引用,以获取所需的 TabPage 控件并返回它。因此,从数据角度来看,异步部分是 ViewModel 的所有就绪部分,但在渲染时最初不会使用。我唯一需要更改以使其同步工作的是 TabControl 的编辑器模板。

提交由部分(异步)视图组成的表单

我终于通过修改一些东西让它工作:

首先,我扩展了我的ModelBinder,以便它处理List,在我阅读了DefaultModelBinder的源代码后,我通过覆盖BindProperty来实现这一点

public class ControlViewModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        ValueProviderResult formIdResult = bindingContext.ValueProvider.GetValue("FormId");
        if (formIdResult == null)
            return null;
        long formId = long.Parse(formIdResult.AttemptedValue);
        ValueProviderResult controlIdResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + ".Id");
        if (controlIdResult == null)
            return null;
        int controlId = int.Parse(controlIdResult.AttemptedValue);
        FormViewModel form;
        ControlViewModel model = FormControlManager.GetControl(formId, controlId, out form);
        if (model == null)
            return null;
        bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType());
        return base.BindModel(controllerContext, bindingContext);
    }

    protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
    {
        // need to skip properties that aren't part of the request, else we might hit a StackOverflowException
        string fullPropertyKey = _buildPropName(bindingContext.ModelName, propertyDescriptor.Name);
        if (!bindingContext.ValueProvider.ContainsPrefix(fullPropertyKey))
        {
            return;
        }
        // call into the property's model binder
        object originalPropertyValue = propertyDescriptor.GetValue(bindingContext.Model);
        ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name];
        // If it is not an IEnumerable we can make the default Function do its magic
        if (!(originalPropertyValue is IEnumerable<ControlViewModel>))
            base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
        else
        {
            // Else we need to call the BindModel method for each Member of the Sequence.
            int index = 0;
            foreach (ControlViewModel value in (IEnumerable<ControlViewModel>)originalPropertyValue)
            {
                propertyMetadata.Model = value;
                ModelBindingContext innerBindingContext = new ModelBindingContext()
                {
                    ModelMetadata = propertyMetadata,
                    ModelName = fullPropertyKey + String.Format("[{0}]", index++),
                    ModelState = bindingContext.ModelState,
                    ValueProvider = bindingContext.ValueProvider
                };
                BindModel(controllerContext, innerBindingContext);
            }
        }
    }
    static string _buildPropName(string prefix, string propertyName)
    {
        if (String.IsNullOrEmpty(prefix))
        {
            return propertyName;
        }
        else if (String.IsNullOrEmpty(propertyName))
        {
            return prefix;
        }
        else
        {
            return prefix + "." + propertyName;
        }
    }

第二部分是,我必须修改要异步呈现的部分的 HtmlHelper。

public static MvcHtmlString EditorForDynamic<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
        where TModel : IAsyncControlWithPrefix
{
    IAsyncControlWithPrefix model = htmlHelper.ViewData.Model;
    htmlHelper.ViewData.TemplateInfo.HtmlFieldPrefix = model.PrefixForPartialRender + htmlHelper.ViewData.TemplateInfo.HtmlFieldPrefix;
    return htmlHelper.EditorFor(expression);
}
public static string GetFullText<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
{
    return htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(ExpressionHelper.GetExpressionText(expression));
}

这样可以确保这些异步渲染的部分仍然创建看起来像的完整嵌套字符串:

儿童[0]。页数[0]。行[0]。细胞[0]。发短信而不仅仅是行[0]。细胞[0]。发短信

这是界面

public interface IAsyncControlWithPrefix
{
    string PrefixForPartialRender { get; set; }
}

我的情况是每个选项卡页面都实现了此接口,因此我可以异步加载每个选项卡页面。

选项卡页需要获取前缀分配,这发生在 TabControlView 中

喜欢这个:

@model {...}.TabControlViewModel
<div class="tabs">
<ul>
    @for (int i = 0; i < Model.Pages.Count; i++)
    {
      <li>
          @Html.ActionLink(Model.Pages[i].Title, "ShowPartial", new  { formId = Model.FormId, controlId = Model.Pages[i].Id})
          @{ Model.Pages[i].PrefixForPartialRender = Html.GetFullText(m => m.Pages[i]);}
      </li>
    }
</ul>
</div>

好吧,现在它就像一个魅力。它可以一次从不同的异步加载选项卡提交数据。相当不错。