rebelliard
rebelliard

Reputation: 9611

Reuse model data in a post action

In my viewmodel, I have a list of items I fetch from the database and then send to the view. I would like to know if it's possible to avoid having to refill the options property whenever I hit a Post action and need to return the model (for validation errors and what not)?

In webforms, this wouldn't be necessary.

Edit: I was not clear. My problem is with the SelectList options I use for my DropDownLists. Everything gets posted, but if I have to return to the view (model is invalid), I have to reload the options from the database! I want to know if this can be avoided.

My viewmodel:

public class TestModel
{
    public TestModel()
    {
        Departments = new List<SelectListItem>();
    }

    public string Name { get; set; }
    public int Department { get; set; }
    public IEnumerable<SelectListItem> Departments { get; set; }
}

My view:

@model MvcApplication1.Models.TestModel    
@using (Html.BeginForm())
{
    @Html.TextBoxFor(m => m.Name)

    @Html.DropDownListFor(m => m.Department, Model.Departments)
    
    <input type=submit value=Submit />
}

My controller (do notice the comment on HttpPost):

public ActionResult Index()
{
    TestModel model = new TestModel
    {
        Name = "Rafael",
        Department = 1,
        Departments = new List<SelectListItem>
        {
            new SelectListItem { Text = "Sales", Value = "1" },
            new SelectListItem { Text = "Marketing", Value = "2", Selected = true },
            new SelectListItem { Text = "Development", Value = "3" }
        }
    };

    // Departments gets filled from a database.

    return View(model);
}

[HttpPost]
public ActionResult Index(TestModel model)
{
if (!ModelState.IsValid)
{
    //Do I have to fill model.Departments again!?!?!?
    
    return View(model); 
}
else { ...  }
}

Thanks in advance.

Edit: FYI, my solution was to use the Session variable.

Upvotes: 5

Views: 478

Answers (3)

Stephen Swensen
Stephen Swensen

Reputation: 22307

I am surprised this question doesn't come up more often, and I am also surprised the obvious (IMHO) answer isn't standard practice these days: nearly all POSTs should be Ajax-based. This solves a whole slew of problems including

  1. No need to repopulate form data when you have e.g. a validation error, or application error (exception). This is particularly desirable when you have client-side state (in true rich web application fashion).
  2. No compulsion to perform client-side validation. Validation can be 100% server-side (where it must be anyways) and the user experience is nearly the same.

Of course, there is some initial work you need to do to build out a framework for this, for example, I have a set of AjaxUpdate, AjaxNothing, AjaxRedirect, AjaxErrors ... ActionResult types which render Json which is processed by some custom Javascript. But once you get that in place, it's smooth sailing.

Upvotes: 0

M.Babcock
M.Babcock

Reputation: 18965

I encountered a similar problem when trying to create an Order wizard in MVC (one where each page of the wizard is implemented as a partial view loaded by AJAX). I highly doubt it is the suggested method but my way of solving this was to call a custom MergeChanges method in each action called by my wizard:

public Order MergeChanges(Order newOrder)
{
    var sessionHistory = (List<string>)Session["sessionHistory"];

    if (sessionHistory == null || sessionHistory.Count == 0)
    return MergeChanges(newOrder, -1);

    return MergeChanges(newOrder, MasterViewController.GetStepNumberByName(sessionHistory.Last()));
}

public Order MergeChanges(Order newOrder, int step)
{
    PreMerge(newOrder);

    Order result = null;
    try
    {
        ApplyLookups(ref newOrder);
        Order oldOrder = (Order)Session["order"];

        if (oldOrder == null)
        {
             Session["order"] = newOrder;
             result = newOrder;
        }
        else
        {
            List<TypeHelper.DecoratedProperty<ModelPageAttribute>> props = null;
            newOrder.GetType().GetDecoratedProperty<ModelPageAttribute>(ref props);
            props = props.Where(p => (p.Attributes.Count() > 0 && p.Attributes.First().PageNumber.Contains(step))).ToList();
            foreach (var propPair in props)
            {
                object oldObj = oldOrder;
                object newObj = newOrder;
                if (!string.IsNullOrEmpty(propPair.PropertyPath))
                {
                    bool badProp = false;
                    foreach (string propStr in propPair.PropertyPath.Split('\\'))
                    {
                        var prop = oldObj.GetType().GetProperty(propStr);
                        if (prop == null)
                        {
                            badProp = true;
                            break;
                        }

                        oldObj = prop.GetValue(oldObj, BindingFlags.GetProperty, null, null, null);
                        newObj = prop.GetValue(newObj, BindingFlags.GetProperty, null, null, null);
                     }
                     if (badProp)
                          continue;
                 }

                 if (newObj == null)
                     continue;

                 var srcVal = propPair.Property.GetValue(newObj, BindingFlags.GetProperty, null, null, null);
                 var dstVal = propPair.Property.GetValue(oldObj, BindingFlags.GetProperty, null, null, null);

                  var mergeHelperAttr = propPair.Property.GetAttribute<MergeHelperAttribute>();
                   if (mergeHelperAttr == null)
                   {
                        if (newObj != null)
                            propPair.Property.SetValue(oldObj, srcVal, BindingFlags.SetProperty, null, null, null);
                   }
                   else
                   {
                       var mergeHelper = (IMergeHelper)Activator.CreateInstance(mergeHelperAttr.HelperType);
                       if (mergeHelper == null)
                           continue;

                       mergeHelper.Merge(context, HttpContext.Request, newObj, propPair.Property, srcVal, oldObj, propPair.Property, dstVal);
                    }
               }
               result = oldOrder;
          }
    }
    finally
    {
    PostMerge(result);
    }
    return result;
}

Since my case was doing this with a wizard, only specific values applied to each page so in order to only account for properties known to the current page of the wizard, I've implemented some attributes, a (admittedly over complex) ViewController layer, and a custom validation layer. I can share some more code but the code above does the grunt work if you aren't in such a complex situation. If there is a better way, I hope to learn it from the answers to this question because this was a PITA.

Upvotes: 0

jhsowter
jhsowter

Reputation: 619

Just need to strongly type your view, and change your controller method to have a parameter of that class type.

That is, the view

@model MyNamesspace.Models.MyModel
...
@using (Html.BeginForm())
{
    ....
}

And you controller method which is posted to.

[HttpPost]
public ActionResult MyAction(MyModel model)
{
    ...
}

EDIT: Also make sure you have form fields for each property of the model which you need posted to the controller. My example is using Razor too BTW.

Upvotes: 1

Related Questions