Using an interface as the model type of a partial view + data annotations

10,507

Solution 1

My idea isn't going to work: this thread reminded me that classes don't inherit attributes from their interfaces. (As the answer points out, what would happen if two interfaces specified the same property with different attributes, and both were implemented by one class?)

It might work with a common base class. I will try that tomorrow.

Thanks, everybody.

Solution 2

Ann, you're right. I've deleted my comment. You can't post an interface back through your view. However, I don't know what exactly your trying to do since I can't see your code. Maybe something like this? I'm passing an interface to the view, but passing it back as the class I'm expecting. Again, I'm not sure the application is here.

Let's say you have classes like this:

[MetadataType(typeof(PersonMetaData))]
public class Customer : IPerson {
    public int ID { get; set; }
    public string Name { get; set; }
     [Display(Name = "Customer Name")]
    public string CustomerName { get; set; }
}

public class Agent : IPerson {
    public int ID { get; set; }
    public string Name { get; set; }
}

public partial class PersonMetaData : IPerson {
    [Required]
    public int ID { get; set; }

    [Required]
    [Display(Name="Full Name")]
    public string Name { get; set; }
}

public interface IPerson {
    int ID { get; set; }
    string Name { get; set; }
}

public interface IAgent {
    int AgentType { get; set; }
}

public interface ICustomer {
    int CustomerType { get; set; }
}

Your Controller looks like:

    public ActionResult InterfaceView() {
        IPerson person = new Customer {
            ID = 1
        };
        return View(person);
    }

    [HttpPost]
    public ActionResult InterfaceView(Customer person) {
        if (ModelState.IsValid) {
            TempData["message"] = string.Format("You posted back Customer Name {0} with an ID of {1} for the name: {2}", person.CustomerName, person.ID, person.Name);
        }
        return View();
    }

And your View Looks like this:

@model DataTablesExample.Controllers.Customer

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

@if (@TempData["message"] != null) {
    <p>@TempData["message"]</p>
}

@using (Html.BeginForm()) {
    @Html.ValidationSummary(true)
    <fieldset>
        <legend>IPerson</legend>

        @Html.HiddenFor(model => model.ID)

        <div class="editor-label">
            @Html.LabelFor(model => model.Name)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Name)
            @Html.ValidationMessageFor(model => model.Name)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.CustomerName)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.CustomerName)
            @Html.ValidationMessageFor(model => model.CustomerName)
        </div>

        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>
}

Solution 3

Well, actually you have a very reasonable idea! and can be archived is you use the non generic version of the HtmlHelper methods (ex. "@Html.Editor" instead of "@Html.EditorFor"), because the generic versions recreate the ModelMetadata (i don't know why!) based on the generic parameter type and don't use the ModelMetadata of the view. Freaking awful, isn't it?

Hope this help.

Share:
10,507
Ann L.
Author by

Ann L.

I love to code. I have over ten years of experience in the .NET and SQL Server world, and have recently been branching out into other databases, like MongoDB, and into Python. I have published over seven articles, mostly on database subjects. I blog at www.annlewkowicz.com.

Updated on June 30, 2022

Comments

  • Ann L.
    Ann L. almost 2 years

    I have a case where a complex partial view needs different validation of fields depending on where the partial view is used.

    I thought I could get around this by making the partial view take an interface as the model type and implementing two different ViewModels based on the interface. The data annotations in the two ViewModels would be different. I would then supply an instance of the correct ViewModel to the partial view.

    But what I'm finding is that the only annotations that are recognized are those on the interface itself. DAs on the interface-implementing ViewModel classes are ignored, even though those are the objects that are being passed as models. So my plan isn't working.

    Is there a way around this? A better approach? I'd prefer not to split the partial view into separate views if I can avoid it.

    ETA: This is an abbreviated version of the partial view, as requested:

    @model IPerson
    @Html.ValidationSummary(false)
    <fieldset>
        <table class="editForm">
            <tr>
                <td class="editor-label">
                    @Html.LabelFor(model => model.FirstName)
                </td>
                <td class="editor-field">
                    @Html.EditorFor(model => model.FirstName)
                    @Html.ValidationMessageFor(model => model.FirstName)
                </td>
                <td class="editor-label">
                    @Html.LabelFor(model => model.LastName)
                </td>
                <td class="editor-field">
                    @Html.EditorFor(model => model.LastName)
                    @Html.ValidationMessageFor(model => model.LastName)
                </td>
            </tr>
         </table>
      <fieldset>
    

    The real partial view is quite long and has a lot of @if statements managing the rendering (or not) of optional sections, but it doesn't do anything tricky.

  • Ann L.
    Ann L. about 12 years
    That is close to what I was attempting to do, except that my view took IPerson rather than Customer as the model type. This was because I was hoping to be able to pass two different derived classes (with different metadata) to the view. But this didn't work: the metadata on the derived class was ignored. After thinking about it overnight I think I'll factor some of the common complex content out of the view into smaller partial views, and have two views instead of trying to share one view between models. Thank you very much for your comments and your answer!
  • Ann L.
    Ann L. about 10 years
    @chrfin I don't think I did. Unfortunately, it was long enough ago that I don't remember how I ended up handling the problem, and I no longer have access to the source code. Sorry I can't be of more help.
  • Erik Philips
    Erik Philips over 9 years
    You can't post an interface back through your view. doesn't make any sense. Did you mean can't post values to a method that takes an interface? You certainly can using a custom model binder. Additionally, just because you use Model/Interface-A to create a view, absolutely does not mean the post method on the controller had to use Model/Interface-A as the parameter.
  • chrilith
    chrilith over 8 years
    Indeed, I can confirm that things work using the non-*For() version of the methods. For instance use Html.textBox("Name", Model.Name) instead of html.TextBoxFor(m => m.Name).