【问题标题】:MVC generic ViewModelMVC 通用视图模型
【发布时间】:2011-06-10 15:08:32
【问题描述】:

简而言之,我希望能够将通用 ViewModel 传递到我的视图中

这是我想要实现的要点的一些简化代码

public interface IPerson
{
    string FirstName {get;}
    string LastName {get;}
}

public class FakePerson : IPerson
{
    public FakePerson()
    {
        FirstName = "Foo";
        LastName = "Bar";
    }

    public string FirstName {get; private set;} 
    public string LastName {get; private set;} 
}

public class HomeViewModel<T> 
    where T : IPerson, new()
{
    public string SomeOtherProperty{ get; set;}
    public T Person { get; private set; }

    public HomeViewModel()
    {
        Person = new T();
    }
}

public class HomeController : Controller {
    public ViewResult Index() {
        return View(new HomeViewModel<FakePerson>());
    }
}

如果我按如下方式创建视图,则所有工作都按预期进行

<%@ Page Language="C#" 
    MasterPageFile="~/Views/Shared/Site.Master"
    Inherits="System.Web.Mvc.ViewPage<HomeViewModel<FakePerson>>" %>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <%: Html.DisplayFor(m=>m.Person.FirstName) %>
    <%: Html.DisplayFor(m=>m.Person.LastName) %>   
</asp:Content>

但是,如果我想传递一些其他 IPerson 实现,我不想在视图中直接依赖 FakePerson,所以我尝试将页面指令更改为

<%@ Page Language="C#" 
    MasterPageFile="~/Views/Shared/Site.Master"
    Inherits="System.Web.Mvc.ViewPage<HomeViewModel<IPerson>>" %>

但这当然行不通,所以,经过一整天的胡闹,我的头发越来越白,不知道下一步该做什么。

有人可以帮忙吗?

[更新]

有人建议我应该使用协变接口;定义一个非通用接口并在视图中使用它。不幸的是,我已经尝试过了,但还有一个额外的含义。我希望 HtmlHelper 函数能够访问可能在 IPerson 派生类中定义的任何数据注释属性

 public class FakePerson : IPerson
 {
    public FakePerson()
    {
        FirstName = "Foo";
        LastName = "Bar";
    }

    [DisplayName("First Name")]
    public string FirstName {get; private set;}

    [DisplayName("Last Name")]
    public string LastName {get; private set;} 
}

因此,虽然以这种方式使用协变接口,但在某种程度上,它可以通过 ViewModel 访问派生类型;由于视图是输入到界面的,因此似乎无法访问属性。

在视图中是否有一种方法可以访问这些属性,也许是反射。 或者是否可以通过其他方式将 View 键入泛型。

【问题讨论】:

  • 您怎么能期望您的视图访问派生数据,但又不想让它依赖于派生数据?换句话说,如果您访问 FakePerson 独有的数据,那么您的视图依赖于 FakePerson,因此您应该使您的模型成为 FakePerson。如果传入其他对象,则访问 FakePerson 时会出错。
  • 无论如何你都不能对接口使用数据注解,因为属性不是继承的(见stackoverflow.com/questions/540749/…

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


【解决方案1】:

我已经创建了一个界面

public  interface ITestModel
    {
         string FirstName { get; set; }
         string LastName { get; set; }
         string Address { get; set; }
        int Age { get; set; }
    }

创建类并继承自该接口

class TestModel : ITestModel
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Address { get; set; }
        public int Age { get; set; }

    }

在控制器中创建类的实例

public ActionResult TestMethod()
        {
            ITestModel testModel;
            testModel = new TestModel();
            testModel.FirstName = "joginder";
            testModel.Address = "Lovely Home";
            testModel.LastName = "singh";
            testModel.Age = 36;
            return View("testMethod", testModel);
        }

以这种方式创建的视图

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<TestProject.ITestModel>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    TestMethod
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h2>TestMethod</h2>

    <fieldset>
        <legend>Fields</legend>

        <div class="display-label">FirstName</div>
        <div class="display-field"><%: Model.FirstName %></div>

        <div class="display-label">LastName</div>
        <div class="display-field"><%: Model.LastName %></div>

        <div class="display-label">Address</div>
        <div class="display-field"><%: Model.Address %></div>

        <div class="display-label">Age</div>
        <div class="display-field"><%: Model.Age %></div>

    </fieldset>
    <p>
        <%: Html.ActionLink("Edit", "Edit", new { /* id=Model.PrimaryKey */ }) %> |
        <%: Html.ActionLink("Back to List", "Index") %>
    </p>

</asp:Content>

现在我可以传递任何类似接口的任何对象

我希望它会有所帮助:)

【讨论】:

    【解决方案2】:

    我已经成功地让协变工作,即将视图绑定到一个抽象基类。事实上,我所拥有的是与 List 的绑定。然后我为每个子类创建一个强类型化的特定视图。这负责绑定。

    但是重新绑定会失败,因为 DefaultModelBinder 只知道抽象基类,你会得到一个异常,比如“无法创建抽象类”。解决方案是在你的基类上有一个属性,如下所示:

        public virtual string BindingType
        {
            get
            {
                return this.GetType().AssemblyQualifiedName;
            }
        }
    

    将其绑定到视图中的隐藏输入。然后用 Global.asax 中的自定义替换默认 ModelBinder:

        // Replace default model binder with one that can deal with BaseParameter, etc.
        ModelBinders.Binders.DefaultBinder = new CustomModelBinder();
    

    在您的自定义模型绑定器中,您拦截绑定。如果它是用于您已知的抽象类型之一,则解析 BindingType 属性并替换模型类型,以便获得子类的实例:

        public class CustomModelBinder : DefaultModelBinder
    {
        private static readonly ILog logger = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
    
        protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, System.Type modelType)
        {
            if (modelType.IsInterface || modelType.IsAbstract)
            {
                // This is our convention for specifying the actual type of a base type or interface.
                string key = string.Format("{0}.{1}", bindingContext.ModelName, Constants.UIKeys.BindingTypeProperty);            
                var boundValue = bindingContext.ValueProvider.GetValue(key);
    
                if (boundValue != null && boundValue.RawValue != null)
                {
                    string newTypeName = ((string[])boundValue.RawValue)[0].ToString();
                    logger.DebugFormat("Found type override {0} for Abstract/Interface type {1}.", modelType.Name, newTypeName);
    
                    try
                    {
                        modelType = System.Type.GetType(newTypeName);
                    }
                    catch (Exception ex)
                    {
                        logger.ErrorFormat("Error trying to create new binding type {0} to replace original type {1}. Error: {2}", newTypeName, modelType.Name, ex.ToString());
                        throw;
                    }
                }
            }
    
            return base.CreateModel(controllerContext, bindingContext, modelType);
        }
    
        protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
        {
            if (propertyDescriptor.ComponentType == typeof(BaseParameter))
            {
                string match = ".StringValue";
                if (bindingContext.ModelName.EndsWith(match))
                {
                    logger.DebugFormat("Try override for BaseParameter StringValue - looking for real type's Value instead.");
                    string pattern = match.Replace(".", @"\.");
                    string key = Regex.Replace(bindingContext.ModelName, pattern, ".Value");
                    var boundValue = bindingContext.ValueProvider.GetValue(key);
                    if (boundValue != null && boundValue.RawValue != null)
                    {
                    // Do some work here to replace the base value with a subclass value...
                        return value;
                    }
                }
            }
    
            return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
        }
    }
    

    在这里,我的抽象类是 BaseParameter,我将 StringValue 属性替换为与子类不同的值(未显示)。

    请注意,尽管您可以重新绑定到正确的类型,但仅与子类关联的表单值不会自动往返,因为模型绑定器只能看到基类上的属性。在我的例子中,我只需要替换 GetValue 中的一个值并从子类中获取它,所以很容易。如果您需要绑定大量子类属性,则需要做更多的工作并将它们从表单 (ValueProvider[0]) 中取出并自己填充实例。

    请注意,您可以为特定类型添加新的模型绑定器,这样您就可以避免泛型类型检查。

    【讨论】:

      【解决方案3】:

      你的代码几乎是正确的;你只需要在控制器中传递一个HomeViewModel&lt;IPerson&gt;(不是FakePerson)。

      您也可以在视图中为模型使用协变接口,但这可能有点过头了。

      【讨论】:

      • 公共类 HomeController : Controller { public ViewResult Index() { return View(new HomeViewModel()); } }
      • public ViewResult Index() { return View(new HomeViewModel());.这会产生运行时错误,因为 IPerson 不是具体类型,关键是能够将 IPerson 的派生类传递给视图
      • 再次感谢。您能否详细说明我如何实现协变接口或指向一些文档
      【解决方案4】:

      让您的 ViewModel 类实现非泛型(或泛型协变)接口,然后修改视图以采用该接口而不是具体类。

      【讨论】:

      • 谢谢。我已经考虑过这一点,只是再次尝试了很好的衡量标准,但是这种方法并没有给我所有我希望的功能。请查看我的问题的更新。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-11-24
      • 1970-01-01
      • 2012-01-15
      • 1970-01-01
      • 2011-05-27
      相关资源
      最近更新 更多