pjobs
pjobs

Reputation: 1247

Asp.net mvc what is the best practice of rebuilding ViewModel?

On POST , if validation failed and before sending back the ViewModel to the same View with Model State errors, do you rebuild ViewModel for all SelectLists, ReadOnly fields etc? right now I have separate methods for Fill First Time(for GET Edit-Method) / Rebuild ViewModels from domain objects, what is the best practice so I can be DRY and also not have to change two methods any time I add a new readonly property to ViewModel?

My Solution: Followed this Pattern

Followed pattern suggested here: https://stackoverflow.com/a/2775656/57132 In IModelBuilder Implementation

Build(..)
{  
   var viewModel = new ViewModel();     
   // and Fill all Non-ReadOnly fields
   ...
   ...
   call CompleteViewModel(viewModel)     
}  

CompleteViewModel(ViewModel viewModel)
{
  //Fill all ReadOnly & SelectLists
  ...
}

The reason I went with this solution is because I don't want to store stuff on server to retrieve across the HTTP Requests

Upvotes: 6

Views: 2629

Answers (3)

M.Ob
M.Ob

Reputation: 1857

I like @LukLed's answer above - it looks very interesting. If you want another option, here's what I currently do.

In my service layer, I have a method to build my view model. I call that on GET and return the the view model to the view. On POST, I build the model from the incoming ID and then TryUpdateModel(model). From there, you can do whatever you like (save, check model state, etc.). With this method, you only have 1 build method and only have to update it once if your model changes (i.e. add/remove properties in the future, etc.).

[HttpGet]
public ActionResult AssessFocuses(int apaID)
{
    var model = this.apaService.BuildAssessFocusesViewModel(apaID);
    return this.View(model);
}

[HttpPost]
public ActionResult AssessFocuses(int apaID, string button)
{
    var model = this.apaService.BuildAssessFocusesViewModel(apaID);
    this.TryUpdateModel(model);

    switch (button)
    {
        case ButtonSubmitValues.Back:
        case ButtonSubmitValues.Next:
        case ButtonSubmitValues.Save:
        case ButtonSubmitValues.SaveAndClose:
            {
                try
                {
                    this.apaService.SaveFocusResults(model);
                }
                catch (ModelStateException<AssessFocusesViewModel> mse)
                {
                    mse.ApplyTo(this.ModelState);
                }

                if (!this.ModelState.IsValid)
                {
                    this.ShowErrorMessage(Resources.ErrorMsg_WEB_ValidationSummaryTitle);
                    return this.View(model);
                }

                break;
            }

        default:
            throw new InvalidOperationException(string.Format(Resources.ErrorMsg_WEB_InvalidButton, button));
    }

    switch (button)
    {
        case ButtonSubmitValues.Back:
            return this.RedirectToActionFor<APAController>(c => c.EnterRecommendationsPartner(model.ApaID));

        case ButtonSubmitValues.Next:
            return this.RedirectToActionFor<APAController>(c => c.AssessCompetenciesPartner(model.ApaID));

        case ButtonSubmitValues.Save:
            this.ShowSuccessMessage(Resources.Msg_WEB_NotifyBarSuccessGeneral);
            return this.RedirectToActionFor<APAController>(c => c.AssessFocuses(model.ApaID));

        case ButtonSubmitValues.SaveAndClose:
        default: 
            return this.RedirectToActionFor<UtilityController>(c => c.CloseWindow());
    }
}

Upvotes: 0

LukLed
LukLed

Reputation: 31862

I don't rebuild it, because I don't stay at POST. I follow POST-REDIRECT-GET pattern, so if I post to /User/Edit/1 using POST HTTP method, I get redirected to /User/Edit/1 uasing GET.

ModelState is transferred to TempData to follow Post-Redirect-Get and be availabe at GET call. View model is built in one place, at GET call. Example:

    [HttpPost]
    [ExportModelStateToTempData]
    public ActionResult Edit(int id, SomeVM postedModel)
    {
        if (ModelState.IsValid) {
            //do something with postedModel and then go back to list
            return RedirectToAction(ControllerActions.List);
        }

        //return back to edit, because there was an error
        return RedirectToAction(ControllerActions.Edit, new { id });
    }

    [ImportModelStateFromTempData]
    public ActionResult Edit(int id)
    {
        var model = //create model here
        return View(ControllerActions.Edit, model);
    }

This is code for attributes importing/exporting ModelState:

public abstract class ModelStateTempDataTransferAttribute : ActionFilterAttribute
{
    protected static readonly string Key = typeof(ModelStateTempDataTransferAttribute).FullName;
}

public class ExportModelStateToTempDataAttribute : ModelStateTempDataTransferAttribute
{
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        //Only export when ModelState is not valid
        if (!filterContext.Controller.ViewData.ModelState.IsValid)
        {
            //Export if we are redirecting
            if ((filterContext.Result is RedirectResult) || (filterContext.Result is RedirectToRouteResult))
            {
                filterContext.Controller.TempData[Key] = filterContext.Controller.ViewData.ModelState;
            }
        }

        base.OnActionExecuted(filterContext);
    }
}

public class ImportModelStateFromTempDataAttribute : ModelStateTempDataTransferAttribute
{
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        ModelStateDictionary modelState = filterContext.Controller.TempData[Key] as ModelStateDictionary;

        if (modelState != null)
        {
            //Only Import if we are viewing
            if (filterContext.Result is ViewResult)
            {
                filterContext.Controller.ViewData.ModelState.Merge(modelState);
            }
            else
            {
                //Otherwise remove it.
                filterContext.Controller.TempData.Remove(Key);
            }
        }

        base.OnActionExecuted(filterContext);
    }
}

Upvotes: 7

Bassam Mehanni
Bassam Mehanni

Reputation: 14944

The simplest solution would be to pass in you viewModel to the method and account for null

private MyViewModel BuildViewModel(MyViewModel model = null)
{
    model = model ?? new MyViewModel();
    model.ReadOnlyList = new .....
    .
    .
    return model;
}

for Create:

 var model = BuildViewModel();

for rebuild:

 model = buildViewModel(model);

Upvotes: 1

Related Questions