ABC-Groep

CATEGORIES ASP.NET MVC , WEB POSTED IN C# , TIPS & TRICKS

In ASP.NET MVC you have lots of flexibility in writing your views, controllers and viewmodels. Anything is possible, which is great! But it also has a downside. Everyone finds there own way of doing things and when you are working in a team the codebase may become hard to maintain. I see many applications which have implemented the most basic operations such as Create and Update using 2 views and 2 viewmodels (sometimes 1 viewmodel if you are lucky). I think we can do better than that!

Lets keep the same functionalities and flexibility using only 1 view and 1 viewModel (and a baseViewModel). Additionaly we don't want an IF statement in our view to render the correct form action, this should be handled by our viewmodel.

The setup

We have a database that holds people that our application will use. We want to create new people and update existing ones. To keep it simple I have created a very basic Person class that looks like this:

public class Person : IEntityWithId
{
    public int Id { getset; }
    public string Firstname { getset; }
    public string Lastname { getset; }
}

This doesn't require any explanation except for the interface which is implemented. 
Even though the name says it all here is the definition of IEntityWithId:

public interface IEntityWithId
{
    int Id { getset; }
}

I have created this interface because we will use it in our ViewModel.

Creating the ViewModel

Now we have the boring stuff sorted out, we can start with the interesting part!

Because we want to keep our code consistent, we want to reuse this workflow as much as possible. To make sure we do so, I have created a BaseViewModel.

public abstract class BaseEntityFormViewModel<Twhere T : IEntityWithId
{
    protected T Entity { getset; }
 
    public bool IsUpdate => Entity.Id > 0;
 
    /// <summary>
    /// Gets the action name based on the lambda supplied
    /// </summary>
    /// <typeparam name="TController"></typeparam>
    /// <param name="action"></param>
    /// <returns></returns>
    protected string GetActionName<TController>(Expression<Func<TControllerActionResult>> action) where TController : Controller
    {
        return ((MethodCallExpression)action.Body).Method.Name;
    }
}

Using the generic T, we make sure this viewmodel can be used for all entities which implement IEntityWithId (see the alternative way if you don't like this dependancy). To have access to all the properties on our actual entity I have created a property Entity of type T. Additionally, I have added a property IsUpdate (Get only) that tells us if we are updating the entity or creating it.

The method GetActionName Will help us to determine which action of our controller will have to be called on form post. Thanks to the lambda input parameter of this method, we can avoid hard coding our method name. The way we use this method will become clear below.

Now lets define our PersonFormViewModel.

public class PersonFormViewModel : BaseEntityFormViewModel<Person>
{
    //Create a strongly named property to use in our view
    //If you are ok with Entity in your view, you can make that property public
    public Person Person
    {
        get { return Entity; }
        set { Entity = value; }
    }
 
    public string ActionName
    {
        get
        {
            return IsUpdate
                ? GetActionName<PersonController>(c => c.Update(null))
                : GetActionName<PersonController>(c => c.Create(null));
        }
    }
 
    public PersonFormViewModel()
    {
        Person = new Person();
    }
}

In this ViewModel I have encapsulated the Entity property into Person just to make our code in the view a bit more readable. If you are ok with Entity than you can just remove that line and make the Entity property in our baseViewModel public.

The property ActionName is get only and will return which action we want to use from our controller. As you can see it uses the GetActionName method from our baseViewModel. We use our PersonController (See code below) For the TController parameter and then we define our lambda to tell our method which function we want to use. Don't worry about the null passed to the method in the lambda expression. This method will never be executed, we only want to use its signature. We return the name as a string so we can use it in Html.BeginForm() in our View.

The PersonController

Our controller is pretty straight forward. I have an Index method, and 2 create and update methods, 1 Get and 1 Post using the viewmodel we have just created.

public class PersonController : Controller
{
    public ActionResult Index()
    {
        //TODO get people
        return View();
    }
 
    public ActionResult Create()
    {
        var model = new PersonFormViewModel();
 
        return View("PersonForm", model);
    }
 
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create(PersonFormViewModel model)
    {
        if (!ModelState.IsValid)
            return View("PersonForm", model);
 
        //TODO create person
 
        return RedirectToAction("Index");
    }
 
    public ActionResult Update(int id)
    {
        var model = new PersonFormViewModel();
 
        //TODO get person
        model.Person = new Person
        {
            Id = id,
            Firstname = "Sander",
            Lastname = "Van Looveren"
        };
 
        return View("PersonForm", model);
    }
 
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Update(PersonFormViewModel model)
    {
        if (!ModelState.IsValid)
            return View("PersonForm", model);
 
        //TODO update person
 
        return RedirectToAction("Index");
    }
}

I have added some TODO's for you to implement because these things are beyond the scope of this post. If there is a demand for the actual data access using the UnitOfWork and repository pattern I can devote a post about that. So just let me know and I'll see what I can come up with!

Important: if your entity is bigger and contains some fields which can't be edited by a user make sure you prevent your controller from over-posting! (For more info see this link)

Finally creating our view

We got our viewmodel and controller all set up so, now its time to create our view.

@model WebApp.ViewModels.PersonFormViewModel
@{
    ViewBag.Title = Model.ActionName;
}
 
<h2>@ViewBag.Title.</h2>
 
@using (Html.BeginForm(Model.ActionName, "Person", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
    @Html.AntiForgeryToken()
    if (Model.IsUpdate)
    {
        @Html.HiddenFor(m => m.Person.Id)
    }
    <div class="form-group">
        @Html.LabelFor(m => m.Person.Firstname, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.Person.Firstname, new { @class = "form-control" })
            @Html.ValidationMessageFor(m => m.Person.Firstname)
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.Person.Lastname, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.Person.Lastname, new { @class = "form-control" })
            @Html.ValidationMessageFor(m => m.Person.Lastname)
        </div>
    </div>
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" class="btn btn-default" value="Save" />
        </div>
    </div>
}
 
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

In our view I have created a form using Html.BeginForm, and I have specified the action to post to using the property from our viewmodel. As you can see this produces a clean layout and insures the correct action will be called. There is 1 difference in the form depending on the action we are doing, and that is the hidden field for the Id of our person. I have added an IF statement (using our IsUpdate property) which will only add the hidden input if we are updating the Person.

We have created 1 clean view which will be reused for both create and update. In my option this is way better than having to maintain 2 different views and even 2 different viewmodels when we want to make changes to our entity. I hope this will help you guys with doing the most basic actions, and of course you can build upon this when you want to do more interesting things.

An alternative way

If you don't like to use the interface IEntityWithId or if your entities use different types for the Id, there is an other way of doing things. In that case we will use a different baseViewModel and of course edit our PersonFormViewModel.

public abstract class BaseFormViewModel<T>
{
    public bool IsUpdate => !GetId().Equals(default(T));
 
    protected abstract T GetId();
 
    /// <summary>
    /// Gets the action name based on the lambda supplied
    /// </summary>
    /// <typeparam name="TController"></typeparam>
    /// <param name="action"></param>
    /// <returns></returns>
    protected string GetActionName<TController>(Expression<Func<TController, ActionResult>> action) where TController : Controller
    {
        return ((MethodCallExpression)action.Body).Method.Name;
    }
}

In this new baseViewModel the T parameter no longer corresponds to the type of our entity, but to the type of the Id parameter of our entity. I have created an abstract method which will return the value of the id. Using this method we can check if the id equals to the default value or if it has been set. This way we can still determine if we are doing an update or creating a new entity, and our View doesn't have to be changed.

To make this post complete I have edited our PersonViewModel to use this new baseViewModel.

public class PersonFormViewModel : BaseFormViewModel<int>
{
    //public Person Person { get; set; }
 
    public int Id { getset; }
    public string Firstname { getset; }
    public string Lastname { getset; }
 
    protected override int GetId()
    {
        //return Person.Id;
        return Id;
    }
 
    public string ActionName
    {
        get
        {
            return IsUpdate
                ? GetActionName<PersonController>(c => c.Update(null))
                : GetActionName<PersonController>(c => c.Create(null));
        }
    }
}

Here you can see that I have commented out the actual entity, and copied the properties from the entity which the user can edit. This way we don't have to worry about the over-posting issue I mentioned earlier. If this is not an issue for you, you can use the property for your entity instead. I just gave you 2 options, its up to you to decide which one fits your needs.

Thanks for reading! Hopefully this has given you some idea's on how to improve your views & viewmodels in MVC. If you have any comments or suggestions, please share!

 

Door: Sander

https://blog.sandervanlooveren.be/posts/mvc-best-practices-for-create-update/

 

Wij gebruiken cookies om ervoor te zorgen dat onze website voor de bezoeker beter werkt. Daarnaast gebruiken wij o.a. cookies voor onze webstatistieken. Meer informatie