6

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

Community
  • 1
  • 1
pjobs
  • 1,247
  • 12
  • 14

3 Answers3

7

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);
    }
}
LukLed
  • 31,452
  • 17
  • 82
  • 107
1

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);
Bassam Mehanni
  • 14,796
  • 2
  • 33
  • 41
  • but I thought that would overwrite changes made by user, unless we check metadata(attributes) for read-only fields , select lists and only update them. – pjobs Jan 14 '13 at 20:11
  • @pjobs that shouldn't override any user input if u r only updating the readonly fields! if u pass in a model filled with user data, u'll get back the same model and only properties that you override will get updated! – Bassam Mehanni Jan 14 '13 at 20:18
  • ok, got it, divide viewmodel creation into two methods, One that fills Read-only properties and another for Not-Read-only properties, and in case of POST, call the first one like you said. – pjobs Jan 14 '13 at 20:35
  • @pjobs u could do that... but what I am saying is that, this method is good for both situation ... it will not override the non-readonly properties, when u pass it a model, it will just fill in the readonly properties and return back the same model that was passed in – Bassam Mehanni Jan 14 '13 at 20:40
  • sorry I think I may have confused you when I said CreateNew/Rebuild, Here when I say Create-New what I meant was Fill the ViewModel for the Edit(GET) Method. – pjobs Jan 14 '13 at 20:44
  • if you gonna be initially filling in the readonly fields then do that in your `GET` method after calling the `BuildViewModel()` method – Bassam Mehanni Jan 14 '13 at 20:47
0

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());
    }
}
M.Ob
  • 1,785
  • 14
  • 27