Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Generic ViewModels in MVC

0.00/5 (No votes)
20 Sep 2017 1  
MVC supports complex objects as Models, including generics. We can use this capability to pass a standard Model type between our Views and Controllers. We can write reusable code to manipulate these standard models. This can change the way we use MVC.

Introduction

In MVC applications we commonly us simple objects as Models in our Views. MVC supports complex objects as Models, including generics. We can use this capability to pass a standard Model type between our Views and Controllers. By having a standard Model type, we can write reusable code to manipulate our Models. This can change the way we use MVC.

This article introduces using this capability. It illustrates a message-driven approach to MVC with simple state management.

State Management

State management is one of the most problematic areas of software design. The stateless nature of http makes this even more complicated for web applications.

In a typical web application there are three object states to deal with - the state of the object when it was read and sent to the user, the state of the object returned from the user, and the state of the object in the data store when you get the modified object back from the user. Most MVC applications ignore the original state of the object. This is because most of them modify the view model. A typical MVC lifecycle is:

  1. Read the database and get the original values
  2. Create the ViewModel and send it to the user
  3. The user modifies the ViewModel and sends it back.
  4. The application reads the database again
  5. The current database value and the user ViewModel are compared to see if the state has changed.
  6. If the persisted object has changed, determine a new persisted object and save it.

Problems

There is a subtle problem. The changes a user made are based on the state of the object they received. This may be different then the object you have just read. The user response might be different if they saw the current object. Updating the database based on the returned value could result in bad data. To make sound decisions all three states are required.

Solutions

A common approach to preserving original state is be to cache the model on the server before sending it. This will work, but requires strategies to store and fetch data, and to purge aged data. Another approach is to use (what I call) Symmetric Models. Symmetric Models are essentially pairs of identical model types. This approach sends and returns two models for the view. The returned models are the original model and the updated model. This provides simple state management. Working with models pairs provides other capabilities, like providing dynamic default values.

Compound Models

In an MVC applications we are usually exchanging simple "data" objects (our Models) between our Views and Controllers. Our forms and tags display the values from the model, accept changes, and send the changed model back. We tell the system the type of object we are using with the @model directive. The objects we send and receive are usually pretty flat, but they can be as complex as we like. They can be generics.

Razor pages only allow a single object in the @model directive. Because this object can be complex, this is not a problem. In a message-driven system, we send message between endpoints. We will use the idea of a message in this example, and build a generic message class: Message<TIn, TOut>. Our payload will be a Person: Person { string name, int age}. We can now create, send, and receive person messages -  return View("ViewName",  new Message<Person, Person>()), @model Message<Person, Person>, public ViewResult PersonHandler(Message<Person, Person> message){}. If we prefer, we can create message types  - PersonMessage : Message<Person, Person>.  The Razor Engine and ModelBinder have no problems handling either. By using Message, we send and receive two models - a view input model (TIn), and a view output model (TOut), held in a container - Message. The message class can contain additional properties, like context or control objects.

Managing State

There are many ways to use compound models. For simple state management, we populate TIn with the information we read from the database, and TOut with an empty Person before sending it to the View. We bind our tags to TOut, and set the initial value from TIn - <input type="text" name="@Model.TOut.Name" value="@Model.TIn.Name"> . MVC will post a Message with the user values in TOut, and (with a little help), the original values in TIn. Once we receive the message on our controller action, we can read the database again and have all three states available. TOut doesn't need to be an empty Person when we send it to the View. We can also send things like default values based on real-time conditions.

Code

To try messaging and symmetrical models create a new MVC project. I recommend an MVC Core 2 / C#7.1 project, but MVC Core and C# 6 should work fine. This likely works on .Net Framework, but i haven't checked. Other articles build on this one and will require Core.

Message Class

The Message class is a generic with two generic types.

public class Message<TIn, TOut> {
    public Message() { }
    public Message(TIn inpart, TOut outpart) {
        Input = inpart;
        Output = outpart;
    }
    public TIn Input { get; set; }
    public TOut Output { get; set; }
}

Person Class

This class is just a payload. You can use any class you want.

public class Person{
    public string Name{ get; set; }
    public int Age{ get; set; }
}

Controller

You can use the default Home controller.  Replace the body this.

public class HomeController : Controller
{
    public IActionResult Index() {
        var inPerson = new Person {
            Name = "Max",
            Age = 22
        };

        var outPerson = new Person();
        var message = new Message<Person, Person>(inPerson, outPerson);

        return View(message);
    }

    [Route("home-message-handler", Name = "Home.MessageHandler")]
    public IActionResult MessageHandler(Message<Person, Person> message){
        return View(message);
    }
}

Input View

Use the Index.cshtml page as for the main form. The upper section of the form has the form fields. MVC will bind to the name attribute of the input tags. It is smart enough know what you are doing, so you only need to use Output.[field]. This hints at the power of this approach - we can write reusable code that works on Output, and use it with any model. You can get intellisense completion if you start with @Model. Delete the @Model afterwards. Set the displayed value using the value attribute. This requires using@Model.Input.[field].

In this simple example, state is managed using a common hidden inputs approach.  One way to create them is to manually create a tag for each field - <input type="hidden" name="Input.Name" value="@Model.Input.Name" />. Another approach is to loop over the object properties using a little C# and generate the tags. In another article I'll show an even better approach using a StateTagHelper.

@using System.Reflection
@model Message<Person, Person>

<form>
    Name: <input type="text" name="Output.Name" value="@Model.Input.Name" />
    Age: <input type="text" name="Output.Age" value="@Model.Input.Age"/>
    <button asp-route="Home.MessageHandler"></button>
    
    @* State *@
    @foreach (PropertyInfo p in Model.Input.GetType().GetProperties()) {
        dynamic value = p.GetValue(@Model.Input);
        dynamic name = p.Name;
        <text> <input type="hidden" name="Input.@name" value="@value" /> </text>
    }
</form>

Aside - Razor pages are really C# classes with html/script inside, and not html page with some C# inside. In the past, it was convenient to look at it the other way. Core, DI in pages, and TagHelpers make the C# nature more apparent and accessible. Viewing pages (.cshtml) as C#, and not html, helps unlock the power of these technologies in your design.

Results View

Create a simple page MessageHandler.cshtml to show the results.

@model Message<Person,Person>

Name: @Model.Input.Name -> @Model.Output.Name<br/>
Age: @Model.Input.Age -> @Model.Output.Age<br />

Run It

Run the application, change the values in the text boxes, and submit the form. You can set a breakpoint in the MessageHandler method and inspect the message to see that it contains the original and new states.

Wrap Up

This article was a brief introduction to the concept of generic models, and using them as a common model type. It showed how a generic model Message<Input, Output> could be created and used to maintain state in a simple CRUD type page.

A core underlying concept is that by creating a generic type like - Container<T1, T2>, we can use these as a standard models for many or all of our views. We can write reusable code that operates on the container or the generic parameters. This was touched on in the examples with the variable Output. Doing this manually is easy and powerful, but TagHelpers take this to another level, as we will see in other articles.

When we use the same object type for T1 and T2 (Symmetric Models) we can implement things like state management or dynamic default values. We can also use an Asymmetric Models approach where the models are different. One use is to return a subset of the Input model in the Output model. This is can be an alternative to using [Bind( on our actions. We can also use dissimilar model types and have our Views become transforms. In all of these case, we can have the Pre and Post objects packaged together. These concepts are explored in other articles.

By passing around Message objects instead of data objects we can write message handling and content-based routing code. This allows us to use MVC not just in the common CRUD mode, but in message-driven or workflow approaches. These scenarios are also explored in later articles.

History

Initial - 9-20-2017.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here