MVC通用ViewModel

时间:2011-01-17 18:27:34

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

简而言之,我希望能够将通用的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>>" %>

但当然这不起作用,所以,经过一整天的捣乱,我有更多的白发,不知道接下来该做什么。

请有人帮忙吗。

[UPDATE]

有人建议我应该使用协变接口;定义非泛型接口并在View中使用它。不幸的是,我尝试了这一点,但有一个附带的含义。我希望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访问派生类型。由于视图是在界面上键入的,因此无法访问属性。

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

4 个答案:

答案 0 :(得分:2)

我已经成功地使用了协变,即将View绑定到抽象基类。事实上,我所拥有的是对List&lt; MyBaseClass&gt;的绑定。然后我创建一个特定的View强类型到每个子类。这照顾了绑定。

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

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

将其绑定到视图中的隐藏输入。然后用Global.asax中的自定义ModelBinder替换默认的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属性替换为与子类不同的值(未显示)。

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

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

答案 1 :(得分:0)

你的代码几乎是正确的;你只需要在控制器中传递HomeViewModel<IPerson>(不是FakePerson)。

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

答案 2 :(得分:0)

使ViewModel类实现非泛型(或通用协变)接口,然后修改视图以获取该接口而不是具体类。

答案 3 :(得分:0)

我创建了一个界面

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>

现在我可以传递任何类似界面的任何对象

我希望它会有所帮助:)