Looking for an alternative to serializing data in controller - asp.net-mvc-3

I am looking for a method to switch from serializing data (which I originally did using Mvc Futures) and passing it around my controller actions to something that does not use serialize. My previous implementation was for a wizard that passed data from action to action until it was submitted and the data was saved. However, I am unable to use serialization in a new project and am looking for an alternative.
Here is an example of what I did in my controller:
private MyViewModel myViewModel;
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
var serialized = Request.Form["myViewModel"];
if (serialized != null) //Form was posted containing serialized data
{
myViewModel = (MyViewModel)new MvcSerializer()
.Deserialize(serialized, SerializationMode.Signed);
TryUpdateModel(myViewModel);
}
else
myViewModel= (MyViewModel)TempData["myViewModel"] ?? new MyViewModel();
TempData.Keep();
}
protected override void OnResultExecuted(ResultExecutedContext filterContext)
{
if (filterContext.Result is RedirectToRouteResult)
TempData["myViewModel"] = myViewModel;
}
Then in some actions:
// STEP 1:
public ActionResult Step1()
{
return View(myViewModel);
}
[HttpPost]
[ActionName("Step1")]
public ActionResult Step1POST(string nextButton)
{
if ((nextButton != null) && ModelState.IsValid)
return RedirectToAction("Step2");
return View(myViewModel);
}
// STEP 2:
[Themed]
public ActionResult Step2()
{
return View(myViewModel);
}
[HttpPost]
[ActionName("Step2")]
public ActionResult Step2POST(string backButton, string nextButton)
{
if (backButton != null)
return RedirectToAction("Step1");
else if ((nextButton != null) && ModelState.IsValid)
return RedirectToAction("Step3");
return View(myViewModel);
}
My view would contain this inside the #Html.BeginForm block:
#Html.Hidden("myViewModel",
new MvcSerializer().Serialize(Model, SerializationMode.Signed))
My first thought is that I have no other alternative (other than maybe jQuery, which I also cannot use right now). In this scenario I have to figure out how to use TempData in each ActionResult, which will get messy if I have 10 or so inputs in each view.
So my question would probably be two-fold:
Is there a clean alternative to using serialize in this manner?
If there is no clean alternative and I am forced to do it in each
ActionResult, I am not sure of how to do so if, for example, I kept
it to one input view and one submit/confirm view and just wanted to
pass two values to the "Submit" step (not shown above), such as
FirstName an EMail. How might I do that using TempData?
Thanks.

Related

Extension methods cannot be dynamically dispatched error - how do I solve this?

Can't find the proper solution to this problem.
I am using [Serializable] (MVC3 Futures) in order to have a "wizard" with separate views. Here is the code in my controller to serialize:
private MyViewModel myData;
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
var serialized = Request.Form["myData"];
if (serialized != null) //Form was posted containing serialized data
{
myData = (MyViewModel)new MvcSerializer().Deserialize(serialized, SerializationMode.Signed);
TryUpdateModel(myData);
}
else
myData = (MyViewModel)TempData["myData"] ?? new MyViewModel();
TempData.Keep();
}
protected override void OnResultExecuted(ResultExecutedContext filterContext)
{
if (filterContext.Result is RedirectToRouteResult)
TempData["myData"] = myData;
}
Further along in my controller I do something like this (just a snippet - code goes through wizard with next and back button strings):
public ActionResult Confirm(string backButton, string nextButton)
{
if (backButton != null)
return RedirectToAction("Details");
else if ((nextButton != null) && ModelState.IsValid)
return RedirectToAction("Submitted");
else
return View(myData);
}
In my .cshtml view, I have this:
#using (Html.BeginFormAntiForgeryPost())
{
#Html.Hidden("myData", new MvcSerializer().Serialize(Model, SerializationMode.Signed))
...
#Html.TextBoxFor(m => model.Step.EMail)
...
}
Because I am using dynamics, I have to use a variable instead in the view:
var model = (MyViewModel) Model.myData;
in order to do the #Html.TextBoxFor above. And herein lies my probelm, because if I do #model MyViewModel instead, then I can't do model.Step.EMail. But because of dynamics, the #Html.Hidden won't work and I get the following error:
Compiler Error Message: CS1973: 'System.Web.Mvc.HtmlHelper'
has no applicable method named 'Hidden' but appears to have an
extension method by that name. Extension methods cannot be dynamically
dispatched. Consider casting the dynamic arguments or calling the
extension method without the extension method syntax.
I can switch to some other way of doing this without [Serializable], but then I have to convert a LOT of code. Is there any way to make this work?
The extension method is not identifying the method because, the data type does not match.
Try cast as object.
#Html.Hidden("myData", new MvcSerializer().Serialize(Model, SerializationMode.Signed) as Object)
or
#Html.Hidden("myData", (Object)new MvcSerializer().Serialize(Model, SerializationMode.Signed))
It will works.
You can call
#(InputExtensions.Hidden(Html, "myData", new MvcSerializer().Serialize(Model, SerializationMode.Signed)))
instead of #Html.Hidden(...)
It is calling the extension method without the extension method syntax.

how to iterate ViewBag or how can I copy the values of viewBag from one Action to another Action

I have a base Controller like follow
public abstract class BaseController
{
protected ActionResult LogOn(LogOnViewModel viewModel)
{
SaveTestCookie();
var returnUrl = "";
if (HttpContext != null && HttpContext.Request != null && HttpContext.Request.UrlReferrer != null)
{
returnUrl = HttpContext.Request.UrlReferrer.LocalPath;
}
TempData["LogOnViewModel"] = viewModel;
return RedirectToAction("ProceedLogOn", new { returnUrl });
}
public ActionResult ProceedLogOn(string returnUrl)
{
if (CookiesEnabled() == false)
{
return RedirectToAction("logon", "Account", new { area = "", returnUrl, actionType, cookiesEnabled = false });
}
var viewModel = TempData["LogOnViewModel"] as LogOnViewModel;
if (viewModel == null)
{
throw new NullReferenceException("LogOnViewModel is not found in tempdata");
}
//Do something
//the problem is I missed the values which are set in the ViewBag
}
}
and another Controller
public class MyController : BaseController
{
[HttpPost]
public ActionResult LogOn(LogOnViewModel viewModel)
{
// base.LogOn is used in differnet controller so I saved some details in view bag
ViewBag.Action = "LogonFromToolbar";
ViewBag.ExtraData = "extra data related only for this action";
return base.LogOn(viewModel);
}
}
the problem is I missed the view bag values in ProceedLogOn action method.
I have the values in Logon method in BaseController.
How can I copy the values of ViewBag from one Action to another Action?
So I can not simply say this.ViewBag=ViewBag;
because ViewBag doesn't have setter. I was thinking of Iterating through viewbag.
I tried ViewBag.GetType().GetFields() and ViewBag.GetType().GetProperties() but they return nothing.
ViewData reflects ViewBag
You can iterate the values you've stored like this :
ViewBag.Message = "Welcome to ASP.NET MVC!";
ViewBag.Answer = 42;
foreach (KeyValuePair<string, object> item in ViewData)
{
// if (item.Key = "Answer") ...
}
This link should also be useful
I'm afraid I don't have the answer how to copy ViewBag.
However, I would never use ViewBag that way.
ViewBag is some data the Controller gives to the View to render output if someone does not like to use ViewModel for some reasons. The View should never know anything about the Controller but your ViewBag is holding a ActionName ;).
Anyway, the ProceedLogOn action method has pretty much parameters which is ... not a nice code actually so why hesitate to add more parameters which are currently being hold in MyController.Logon ViewBag? Then inside method ProceedLogOn you have what you want.
;)

send data between actions with redirectAction and prg pattern

how can i send data between actions with redirectAction??
I am using PRG pattern. And I want to make something like that
[HttpGet]
[ActionName("Success")]
public ActionResult Success(PersonalDataViewModel model)
{
//model ko
if (model == null)
return RedirectToAction("Index", "Account");
//model OK
return View(model);
}
[HttpPost]
[ExportModelStateToTempData]
[ActionName("Success")]
public ActionResult SuccessProcess(PersonalDataViewModel model)
{
if (!ModelState.IsValid)
{
ModelState.AddModelError("", "Error");
return RedirectToAction("Index", "Account");
}
//model OK
return RedirectToAction("Success", new PersonalDataViewModel() { BadgeData = this.GetBadgeData });
}
When redirect you can only pass query string values. Not entire complex objects:
return RedirectToAction("Success", new {
prop1 = model.Prop1,
prop2 = model.Prop2,
...
});
This works only with scalar values. So you need to ensure that you include every property that you need in the query string, otherwise it will be lost in the redirect.
Another possibility is to persist your model somewhere on the server (like a database or something) and when redirecting only pass the id which will allow to retrieve the model back:
int id = StoreModel(model);
return RedirectToAction("Success", new { id = id });
and inside the Success action retrieve the model back:
public ActionResult Success(int id)
{
var model = GetModel(id);
...
}
Yet another possibility is to use TempData although personally I don't recommend it:
TempData["model"] = model;
return RedirectToAction("Success");
and inside the Success action fetch it from TempData:
var model = TempData["model"] as PersonalDataViewModel;
You cannot pass data between actions using objects, as Darin mentioned, you can only pass scalar values.
If your data is too large, or does not consist only of scalar values, you should do something like this
[HttpGet]
public ActionResult Success(int? id)
{
if (!(id.HasValue))
return RedirectToAction("Index", "Account");
//id OK
model = LoadModelById(id.Value);
return View(model);
}
And pass that id from RedirectToAction
return RedirectToAction("Success", { id = Model.Id });
RedirectToAction method returns an HTTP 302 response to the browser, which causes the browser to make a GET request to the specified action. So you can not pass complex objects like you calling other methods with complex objects.
Your possible solution is to pass an id using with the GET action can build the object again. Some thing like this
[HttpPost]
public ActionResult SuccessProcess(PersonViewModel model)
{
//Some thing is Posted (P)
if(ModelState.IsValid)
{
//Save the data and Redirect (R)
return RedirectToAction("Index",new { id=model.ID});
}
return View(model)
}
public ActionResult Index(int id)
{
//Lets do a GET (G) request like browser requesting for any time with ID
PersonViewModel model=GetPersonFromID(id);
return View(id);
}
}
You can keep data (The complex object) between This Post and GET request using Session also (TempData is internally using session even). But i believe that Takes away the purity of PRG Pattern.

MVC3 Return View By Default

Basically, I was wondering if anyone knows of a way that you can set up MVC3 in a way that it will first look for an action, and if none exists, it will automatically return the view at that location. Otherwise each time I make a page, I will have to rebuild it after adding the action.
It isn't something that's stopping the project from working nor is it an issue, it would just be a very nice thing to include in the code to help with speed of testing more than anything.
EDIT:
Just for clarity purposes, this is what I do every time I create a view that doesn't have any logic inside it:
public ActionResult ActionX()
{
return View();
}
Sometimes I will want some logic inside the action, but majority of the time for blank pages I will just want the above code.
I would like it if there was any way to always return the above code for every Controller/Action combination, UNLESS I have already made an action, then it should use the Action that I have specified.
Thanks,
Jake
Why not just create a single action for this. This will look for a view with the specified name and return a 404 if it doesn't exist.
[HttpGet]
public ActionResult Page(string page)
{
ViewEngineResult result = ViewEngines.Engines.FindView(ControllerContext, page, null);
if (result == null)
{
return HttpNotFound();
}
return View(page);
}
Then make your default route fall back to this:
routes.MapRoute("", "{page}", new { controller = "Home", action = "Page" });
So a request to http://yoursite.com/somepage will invoke Page("somepage")
I'm not altogether sure how useful this will be (or whether its really a good idea) but I guess if you have pages which are purely static content (but maybe use a layout or something so you can't use static html) it could be useful
This is how it could be done though anyway (as a base class, but it doesn't have to be)
public abstract class BaseController : Controller
{
public ActionResult Default()
{
return View();
}
protected override IActionInvoker CreateActionInvoker()
{
return new DefaultActionInvoker();
}
private class DefaultActionInvoker : ControllerActionInvoker
{
protected override ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName)
{
var actionDescriptor = base.FindAction(controllerContext, controllerDescriptor, actionName);
if (actionDescriptor == null)
actionDescriptor = base.FindAction(controllerContext, controllerDescriptor, "Default");
return actionDescriptor;
}
}
}

RedirectToAction after validation errors

If I have the usual Edit actions, one for GET to retrieve an object by it's ID and to display it in an edit form. The next for POST to take the values in the ViewModel and update the object in the database.
public virtual ActionResult Edit(int id)
[HttpPost]
public ActionResult Edit(VehicleVariantEditSaveViewModel viewModel)
If an error occurs during model binding in the POST action, I understand I can RedirectToAction back to the GET action and preserve the ModelState validation errors by copying it to TempData and retrieving it after the redirect in the GET action.
if (TempData["ViewData"] != null)
{
ViewData = (ViewDataDictionary)TempData["ViewData"];
}
How do I then convert that ViewData, which includes the previous invalid ModelState, into a new model to send to the view so the user sees their invalid input with validation warnings? Oddly enough if I pass in a new instance of my ViewModel retrieved from the database (with the original valid data) to the View() this is ignored and the (invalid) data in the ViewData is displayed!
Thanks
I had a similar problem and decided to use the following pattern:
public ActionResult PersonalRecord(Guid id)
{
if (TempData["Model"] == null)
{
var personalRecord = _context.PersonalRecords.Single(p => p.UserId == id);
var model = personalRecord.ToPersonalRecordModel();
return View(model);
}
else
{
ViewData = (ViewDataDictionary) TempData["ViewData"];
return View(TempData["Model"]);
}
}
[HttpPost]
public ActionResult PersonalRecord(PersonalRecordModel model)
{
try
{
if (ModelState.IsValid)
{
var personalRecord = _context.PersonalRecords.Single(u => u.UserId == model.UserId);
personalRecord.Email = model.Email;
personalRecord.DOB = model.DOB;
personalRecord.PrimaryPhone = model.PrimaryPhone;
_context.Update(personalRecord);
_context.SaveChanges();
return RedirectToAction("PersonalRecord");
}
}
catch (DbEntityValidationException ex)
{
var errors = ex.EntityValidationErrors.First();
foreach (var propertyError in errors.ValidationErrors)
{
ModelState.AddModelError(propertyError.PropertyName, propertyError.ErrorMessage);
}
}
TempData["Model"] = model;
TempData["ViewData"] = ViewData;
return RedirectToAction("PersonalRecord", new { id = model.UserId });
}
Hope this helps.
I noticed that the Model is included in ViewData so you don't need to pass it in addition to the ViewData, what I don't understand is how you get at it to then return it to the view.
public ViewResult Edit(int id)
{
// Check if we have ViewData in the session from a previous attempt which failed validation
if (TempData["ViewData"] != null)
{
ViewData = (ViewDataDictionary)TempData["ViewData"];
}
VehicleVariantEditViewModel viewModel = new VehicleVariantControllerViewModelBuilder()
.BuildForEdit(id);
return View(viewModel);
}
The above works but obviously it's making an unnecessary call to the database to build a new Model (which gets automagically overwritten with the invalid values from the Model in the passed ViewData)
Confusing.

Resources