Trouble passing complex data between view and controller in ASP.NET MVC - asp.net

Here's a simplification of my real models in ASP.NET MVC, that I think will help focus in on the problem:
Let's say I have these two domain objects:
public class ObjectA
{
public ObjectB ObjectB;
}
public class ObjectB
{
}
I also have a view that will allow me to create a new ObjectA and that includes selecting one ObjectB from a list of possible ObjectBs.
I have created a new class to decorate ObjectA with this list of possibilities, this is really my view model I guess.
public class ObjectAViewModel
{
public ObjectA ObjectA { get; private set; }
public SelectList PossibleSelectionsForObjectB { get; private set; }
public ObjectAViewModel(ObjectA objectA, IEnumerable<Location> possibleObjectBs)
{
ObjectA = objectA;
PossibleSelectionsForObjectB = new SelectList(possibleObjectBs, ObjectA.ObjectB);
}
}
Now, what is the best way to construct my view and controller to allow a user to select an ObjectB in the view, and then have the controller save ObjectA with that ObjectB selection (ObjectB already exists and is saved)?
I tried creating a strongly-typed view of type, ObjectAViewModel, and binding a Html.DropDownList to the Model.PossibleSelectionsForObjectB. This is fine, and the I can select the object just fine. But getting it back to the controller is where I am struggling.
Attempted solution 1:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(ObjectAViewModel objectAViewModel)
This problem here is that the objectAViewModel.ObjectA.ObjectB property is null. I was thinking the DropDownList which is bound to this property, would update the model when the user selected this in the view, but it's not for some reason.
Attempted solution 2:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(ObjectA objectA)
This problem here is that the ObjectA.ObjectB property is null. Again, I thought maybe the DropDownList selection would update this.
I have also tried using the UpdateModel method in each of the above solutions, with no luck. Does anyone have any ideas? I'm guessing I'm missing a binding or something somewhere...
Thanks!

I use code as follows:
[HttpPost]
public ActionResult Create([Bind(Exclude = "Id")]ObjectA objectAToCreate)
{
try
{
Repository.AddObjectA(objectAToCreate);
return RedirectToAction("Details", new { id = objectAToCreate.Id });
}
catch
{
return View();
}
}
With the following code in a Repository (Entity Framework specific):
public void AddObjectA(ObjectA objectAToAdd)
{
objectAToAdd.ObjectB = GetObjectB(objectAToAdd.ObjectB.Id);
_entities.AddToObjectAs(objectAToAdd);
_entities.SaveChanges();
}
public void GetObjectB(int id)
{
return _entities.ObjectBs.FirstOrDefault(m => m.id == id);
}
As per your commments, it is essentially reloading the object from the underlying data service, however I didn't find the need to use the ModelState to access the attempted value.
This is based on a view coded along these lines:
<p>
<%= Html.LabelFor( f => f.ObjectB.Id) %>
<%= Html.DropDownList("ObjectB.Id", new SelectList((IEnumerable)ViewData["ObjectBList"], "Id", "Descriptor"),"") %>
<%= Html.ValidationFor( f => f.ObjectB, "*") %>
</p>
Note that this could be improved to use a strongly typed ViewModel (which I believe you already do) and also to create a custom Editor Template for ObjectB such that the call could be made using:
<%= Html.EditorFor( f => f.ObjectB ) %>

After some more research it doesn't look like this is a case ASP.NET MVC will take care of for me. Perhaps there is a data service binding model I can use (so MVC would automatically grab the appropriate object out of memory, based on what was selected in the dropdown), but for now, I can fix this by handling it in the controller:
Get the selected item from the dropdown using Controller.ModelState
Reload that ObjectB from the underlying data service
Assign that ObjectB to ObjectA.ObjectB
Save ObjectA
So my controller method looks like this now:
Edited based on the comment from LukLed
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(ObjectA objectA, string objectBStr)
{
ObjectB objectB = _objBService.Get(objectBStr);
objectA.ObjectB = objectB;
_objAService.Save(objectA);
return RedirectToAction("Details", new { id = objectA.Id });
}

Related

ASP.NET MVC Cast BaseViewModel to DerivedViewModel

I'm developing a registration flow where user comes and fills 5 pages to complete a process. I decided to have multiple views and one controller and a ProcessNext action method to go step by step. Each time Process Next gets called it gets the origin view and next view. Since each view associated with there own view model i have created a base view model which all view specific view model derived from. Now the issue is, casting is throwing an exception.. here is the sample code
Base View Model
public class BaseViewModel
{
public string viewName;
}
Personal View Model
public class PersonalViewModel : BaseViewModel
{
public string FirstName;
// rest properties comes here
}
Index.cshtml
#Model PersonalViewModel
#using (Html.BeginForm("ProcessNext", "Wizard", FormMethod.Post, new { class = "form-horizontal", role = "form" }))
#Html.TextBoxFor(m => m.FirstName, new { #class = "form-control" })
<input type="submit" class="btn btn-default" value="Register" />
Basically, I'm binding the view with PersonalViewModel here
Now in Controller ProcessNext Action method looks like this.
public ActionResult ProcessNext(BaseViewModel viewModelData)
{
PersonalViewModel per = (PersonalViewModel) viewModelData;
}
This is failing and throwing a type case exception, why?..
My idea is to use only one action method to transform all these derived view model and send to a common class to validate and process. Please help me to get through this issue.. Thanks!
The reason that you see this exception is that your model type is BaseViewModel and not PersonalViewModel. Model binder is the one that creates a model and since your action's model is BaseViewModel it creates a BaseViewModel object.
I would recommend you to create separate actions for each one of your steps. Each action should have its corresponding model. I also think that you should prefer with composition instead of inheritance in this case.
public class FullModel
{
public FirstStepModel FirstStep {get;set;}
public SecondStepModel SecondStep {get;set;}
}
Then once you start your flow (on a first step for example) you can create a FullModel object and store it somewhere (session/cookie/serialize into a text and send to client - it is really up to you).
Then in controller you will have
[HttpGet]
public ActionResult ProcessFirst()
{
HttpContext.Session["FullModel"] = new FullModel(); //at the beginning store full model in session
var firstStepModel = new FirstsStepModel();
return View(firstStepModel) //return a view for first step
}
[HttpPost]
public ActionResult ProcessFirst(FirstStepModel model)
{
if(this.ModelState.IsValid)
{
var fullModel = HttpContext.Session["FullModel"] as FullModel; //assuming that you stored it in session variable with name "FullModel"
if(fullModel == null)
{
//something went wrong and your full model is not in session..
//return some error page
}
fullModel.FirstStep = model;
HttpContext.Session["FullModel"] = fullModel; // update your session with latest model
var secondStepModel = new SecondStepModel();
return View("SecondStepView", secondStepModel) //return a view for second step
}
// model is invalid ...
return View("FirstStepView", model);
}
[HttpPost]
public ActionResult ProcessSecond(SecondStepModel model)
{
var fullModel = HttpContext.Session["FullModel"] as FullModel; //assuming that you stored it in session variable with name "FullModel"
if(fullModel == null)
{
//something went wrong and your full model is not in session..
//return some error page
}
fullModel.SecondStep = model;
HttpContext.Session["FullModel"] = fullModel; // update your session with latest model
var thirdStepModel = new ThirdStepModel();
return View("ThirdStepModel", thirdStepModel); //return a view for a third step
}
Of course you should extract all the shared code to some reusable method.
And it is entirely up to you what persistence technique to use for passing FullModel between the request.
If you still prefer to go with one Action solution you need to create a custom model binder that is going create derived instances based on some data that is passed from the client. Take a look at this thread
I figured it out a generic way to handle this situation using Model Binders. Here it is..
You might need to have a extended model binder from DefaultBinder to implement to return your model type.
public class WizardModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
var viewIdContext = bindingContext.ValueProvider.GetValue("ViewId");
int StepId = 0;
if (!int.TryParse(viewIdContext, out StepId))
throw new InvalidOperationException("Incorrect view identity");
//This is my factory who gave me child view based on the next view id you can play around with this logic to identify which view should be rendered next
var model = WizardFactory.GetViewModel(StepId);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, model.GetType());
bindingContext.ModelMetadata.Model = model;
return model;
}
}
You would register this binder from your gloab asx like
ModelBinders.Binders.Add(typeof(BaseViewModel), new WizardModelBinder());
Thanks to all who responsed to my query..!! Let me know if you guys have any questions.
Happy coding..!!

How to get the boolean value of checkbox in ASP.net MVC 4

I have a checkbox in .cshtml page;
#Html.CheckBox("All", new { #class = "chkBox"})
I want to get the boolean value of this checkbox in my controller class, for that I have tried
bool all = Convert.ToBoolean(collection["All"]);
where collection is the object of FormCollection Class.
But I am getting value of the checkbox as "all", I don't know how to get the checked or unchecked value using formcollection object.
If anyone have any Idea then please tell me. Thanks.
Why not bind it to a model?
public class MyModel {
public bool All {get; set;}
}
In your view just do the following
#html.CheckBoxFor(m=>m.All)
That should do it.
You Can Get that In this way also:
Let us assume your controller name is Index
[HttpPost]
public ActionResult Index(bool All)
{
return View();
}
If "All"(Checkbox) is Checked then All becomes true.
If "All"(Checkbox) is UNchecked then All becomes false.
Based on true or false You can modify your code in your way.
Note:The variable you mentioned in Post Controller Must be same as the name which you given for checkbox in cshtml.
i.e;
#Html.CheckBox("All", new { #class = "chkBox" })
public ActionResult Index(bool All)
That's because you input tag has the value as all. In this case, it's a good pratice to bind the right type, for sample:
public ActionResult Post(bool all)
{
//here you will get the all boolean value, if check or not as boolean
}
Or, better than this, you could create a ViewModel and bind it, for sample:
Add a class wih the fields you want to use on the Models folder.
public class MyViewModel
{
public string Name { get; set; }
public bool All { get; set; }
}
After it, in your View, you just type the View and create the controls using the Html helpers, for sample:
#model Models.MyViewModel
#using (Html.BeginForm("Post", "Home", FormMethod.Post))
{
#Html.TextBoxFor(model => model.Name)
#Html.CheckBoxFor(model => model.All)
<input type="submit" value="Send" />
}
Obs: it's just a sample
And in your controller, you just get a instance of this model, and let asp.net mvc bind it for you as the object:
public ActionResult Post(MyViewModel model)
{
// here you have the model.All as boolean value
}
It's a good pratice to do, because ViewModels represents the fields you need on the View and transfer it between contexts (controllers, views, services, apis, etc..).
You can try this approach. You can get the more information from here asp.net mvc: why is Html.CheckBox generating an additional hidden input
bool all = (collections["all"] != "false");
You need to modify your code like this. Its working: you need to first convert it in string then convert to boolean.
Convert.ToBoolean(collection["All"].ToString());
Thanks Guys for all of your answers, all make sense. But in accordance of achieving the above requirement I have modified my .cshtml code a bit, rather then using razor syntax I am using this in my .cshtml page
<input id="All" name="All" value="true" type="checkbox" class="chkBox" />
Now in my .cs page I have harvested the boolean value as follows;
all = Convert.ToBoolean(collection["All"]);
So, basically what it does is, if the checkbox is left unchecked then it will give value as false otherwise, formcollection will take into account the checkbox and will give the value of all as true.

How ViewBag in ASP.NET MVC works

How does the ASP.NET MVC's ViewBag work? MSDN says it is just an Object, which intrigues me, how does "Magic" properties such as ViewBag.Foo and magic strings ViewBag["Hello"] actually work?
Also, how can I make one and use it in my ASP.NET WebForms app?
Examples would be really appreciated!
ViewBag is of type dynamic but, is internally an System.Dynamic.ExpandoObject()
It is declared like this:
dynamic ViewBag = new System.Dynamic.ExpandoObject();
which is why you can do :
ViewBag.Foo = "Bar";
A Sample Expander Object Code:
public class ExpanderObject : DynamicObject, IDynamicMetaObjectProvider
{
public Dictionary<string, object> objectDictionary;
public ExpanderObject()
{
objectDictionary = new Dictionary<string, object>();
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
object val;
if (objectDictionary.TryGetValue(binder.Name, out val))
{
result = val;
return true;
}
result = null;
return false;
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
try
{
objectDictionary[binder.Name] = value;
return true;
}
catch (Exception ex)
{
return false;
}
}
}
It's a dynamic object, meaning you can add properties to it in the controller, and read them later in the view, because you are essentially creating the object as you do, a feature of the dynamic type. See this MSDN article on dynamics. See this article on it's usage in relation to MVC.
If you wanted to use this for web forms, add a dynamic property to a base page class like so:
public class BasePage : Page
{
public dynamic ViewBagProperty
{
get;
set;
}
}
Have all of your pages inherit from this. You should be able to, in your ASP.NET markup, do:
<%= ViewBagProperty.X %>
That should work. If not, there are ways to work around it.
The ViewBag is an System.Dynamic.ExpandoObject as suggested. The properties in the ViewBag are essentially KeyValue pairs, where you access the value by the key. In this sense these are equivalent:
ViewBag.Foo = "Bar";
ViewBag["Foo"] = "Bar";
ViewBag is used to pass data from Controller Action to view to render the data that being passed. Now you can pass data using between Controller Action and View either by using ViewBag or ViewData.
ViewBag: It is type of Dynamic object, that means you can add new fields to viewbag dynamically and access these fields in the View. You need to initialize the object of viewbag at the time of creating new fields.
e.g:
1. Creating ViewBag:
ViewBag.FirstName="John";
Accessing View:
#ViewBag.FirstName.
ViewBag is of type dynamic. More, you cannot do ViewBag["Foo"]. You will get exception - Cannot apply indexing with [] to an expression of type 'System.Dynamic.DynamicObject'.
Internal implementation of ViewBag actually stores Foo into ViewData["Foo"] (type of ViewDataDictionary), so those 2 are interchangeable. ViewData["Foo"] and ViewBag.Foo.
And scope. ViewBag and ViewData are ment to pass data between Controller's Actions and View it renders.
ViewBag is a dynamic type that allow you to dynamically set or get values and allow you to add any number of additional fields without a strongly-typed class
They allow you to pass data from controller to view.
In controller......
public ActionResult Index()
{
ViewBag.victor = "My name is Victor";
return View();
}
In view
#foreach(string a in ViewBag.victor)
{
.........
}
What I have learnt is that both should have the save dynamic name property ie ViewBag.victor
public dynamic ViewBag
{
get
{
if (_viewBag == null)
{
_viewBag = new DynamicViewData(() => ViewData);
}
return _viewBag;
}
}

Best way to do global viewdata in an area of my ASP.NET MVC site?

I have an several controllers where I want every ActionResult to return the same viewdata. In this case, I know I will always need basic product and employee information.
Right now I've been doing something like this:
public ActionResult ProductBacklog(int id) {
PopulateGlobalData(id);
// do some other things
return View(StrongViewModel);
}
Where PopulateGlobalData() is defined as:
public void PopulateGlobalData(int id) {
ViewData["employeeName"] = employeeRepo.Find(Thread.CurrentPrincipal.Identity.Name).First().FullName;
ViewData["productName"] = productRepo.Find(id).First().Name;
}
This is just pseudo-code so forgive any obvious errors, is there a better way to be doing this? I thought of having my controller inherit a class that pretty much does the same thing you see here, but I didn't see any great advantages to that. It feels like what I'm doing is wrong and unmaintable, what's the best way to go about this?
You could write a custom action filter attribute which will fetch this data and store it in the view model on each action/controller decorated with this attribute.
public class GlobalDataInjectorAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
string id = filterContext.HttpContext.Request["id"];
// TODO: use the id and fetch data
filterContext.Controller.ViewData["employeeName"] = employeeName;
filterContext.Controller.ViewData["productName"] = productName;
base.OnActionExecuted(filterContext);
}
}
Of course it would much cleaner to use a base view model and strongly typed views:
public class GlobalDataInjectorAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
string id = filterContext.HttpContext.Request["id"];
// TODO: use the id and fetch data
var model = filterContext.Controller.ViewData.Model as BaseViewModel;
if (model != null)
{
model.EmployeeName = employeeName;
model.ProductName = productName;
}
base.OnActionExecuted(filterContext);
}
}
Now all that's left is to is to decorate your base controller with this attribute:
[GlobalDataInjector]
public abstract class BaseController: Controller
{ }
There's another more interesting solution which I personally prefer and which involves child actions. Here you define a controller which handles the retrieval of this information:
public class GlobalDataController: Index
{
private readonly IEmployeesRepository _employeesRepository;
private readonly IProductsRepository _productsRepository;
public GlobalDataController(
IEmployeesRepository employeesRepository,
IProductsRepository productsRepository
)
{
// usual constructor DI stuff
_employeesRepository = employeesRepository;
_productsRepository = productsRepository;
}
[ChildActionOnly]
public ActionResult Index(int id)
{
var model = new MyViewModel
{
EmployeeName = _employeesRepository.Find(Thread.CurrentPrincipal.Identity.Name).First().FullName,
ProductName = _productsRepository.Find(id).First().Name;
};
return View(model);
}
}
And now all that's left is to include this wherever needed (probably the master page if global):
<%= Html.Action("Index", "GlobalData", new { id = Request["id"] }) %>
or if the id is part of the routes:
<%= Html.Action("Index", "GlobalData",
new { id = ViewContext.RouteData.GetRequiredString("id") }) %>
I thought of having my controller inherit a class that pretty much does the same thing you see here, but I didn't see any great advantages to that.
This is the way to go, in my opinion. You'd create a base Controller class that would provide this functionality. If you are familiar with the ASP.NET WebForms model then this is similar to creating a custom base Page class.
As to the advantages of putting it in a base class, the main advantages are readability, maintainability and reusability. If you copy and paste the above method into each controller that needs it, you are going to have a more difficult time if, down the road, you need to add new information to the ViewData collection.
In short, anytime you catch yourself copying and pasting code among classes or views in your application you should stop and think about how to put such logic in a single place. For more, read up on DRY - Don't Repeat Yourself.

ASP.NET MVC 2 - How to POST form Model from PartialView?

I have a single model type to wrap up various models I want to use in my view:
public class QuestionViewData {
public Question Question { get; set; }
public IList<Answer> Answers { get; set; }
}
Now, in my question view I pull the data from the Question object - that's fine. Secondly I iterate through all Answer objects and pass them to a partial view:
<% foreach(Answer item in Model.Answers) { %>
<% Html.RenderPartial("ShowAnswer", item); %>
<% } %>
For each answer (in the partial view) I have some action buttons (like ratings). I'm using separate form POST's with hidden fields for every button.
The problem is that I can't post the whole QuestionViewData model to my action method cause I only have the Answer object as model in the partial view. However, I need to return the complete question view from that action that takes QuestionViewData as model.
How do I handle such situations?
Assuming your answers all contain the id of the question, you can post the answers to your controller method, and then populate the rest of your QuestionViewData model type by looking up the questions from the database again. You then return your QuestionViewData object to the view as usual.
As Robert Harvey said, you can look it up from database, but you could also store it in Session:
[HttpGet]
public ActionResult ShowQuestion(int id)
{
var questionModel = new QuestionViewData();
//populate questionModel
Session["CurrentlyHandledQuestion"] = questionModel;
return View(questionModel);
}
[HttpPost]
public ActionResult ManageAnswer(params)
{
var questionModel = (QuestionViewData)Session["CurrentlyHandledQuestion"];
}
Session data can be lost in many situations, so you should think about situation when questionModel is not available anymore in POST action, but it will work ok most of the time.

Resources