How to implement "Edit" method for complex model in asp.net MVC 3 - asp.net

In my mvc 3 web store app I have this model: Items, Products, Shipping, Users tables, where 1 Item contains 1 Product, 1 User, and 1 Shipping. I need to somehow create and edit items. With creating there are not many problems cause I can pass the "fields to fill" as parameters to Crete(Post) method. With editing there is one problem - Null Reference Exception occures. This is my code:
(controller):
I pass model (of type Item) to Edit Post method as a parameter and get product and shipping, that, I hope), I filled with values.
[HttpGet]
public ViewResult Edit(int itemId)
{
Item target = _itemsRepository.GetItem(itemId);
return View(target);
}
[HttpPost]
public ActionResult Edit(Item model)
{
Product p = model.Product;
Shipping s = model.Shipping;
// here goes some validation stuff
if (ModelState.IsValid)
{
_productRepository.UpdateProduct(p);
_shippingRepository.UpdateShipping(s);
return RedirectToAction("Content");
}
return View();
}
Strongly typed view (where I fill the form):
#model WebStore.WebStoreModels.Item
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
<fieldset>
<legend>Product</legend>
#Html.HiddenFor(i => i.ItemId)
<p>Name: </p>
<p>
#Html.EditorFor(i => i.Product.Name)
#Html.ValidationMessageFor(i => i.Product.Name)
</p>
// and so on
</fieldset>
<fieldset>
<legend>Shipping</legend>
<p>Cost: </p>
<p>
#Html.EditorFor(i => i.Shipping.Cost)
#Html.ValidationMessageFor(i => i.Shipping.Cost)
</p>
// so on
</fieldset>
<p>
<input type="submit" value="Save"/>
</p>
}
And when I fill the form and click "safe" button the NullReferenceException occures in the ProductRepository class in UpdateProduct() method:
public class ProductRepository : IProductRepository
{
private WebStoreDataContext _dataContext;
public ProductRepository(WebStoreDataContext dataContext)
{
_dataContext = dataContext;
}
public Product GetProduct(int productId)
{
return _dataContext.Products.SingleOrDefault(p => p.ProductId == productId);
}
public void UpdateProduct(Product p)
{
var dbProduct = GetProduct(p.ProductId);
dbProduct.Name = p.Name; // here the exception occures
dbProduct.Description = p.Description;
dbProduct.Price = p.Price;
dbProduct.Category = p.Category;
dbProduct.SubCategory = p.SubCategory;
_dataContext.SubmitChanges();
}
Seems like I can't use this assignment:
Product p = model.Product;
I also tried: (in view and then assign it to Product in Edit(Post) method)
TempData["product"] = #Model.Product;
And also: (in view)
#Html.Hidden("product", #Model.Product)
And pass it as parameters to Edit(Post) method:
[HttpGet]
public ViewResult Edit(Product p, Shipping s)
I think the problem is associated with model.
Any help would be great, Sorry for so much code)

You need to add hidden ProductId input inside of the form in your View:
#using (Html.BeginForm())
{
#Html.HiddenFor(i => i.Product.ProductId)
...
}
The reason you need to have this line is that when you submit your form model binder looks at every input (including hidden inputs) and binds them to the properties of your Item model that you pass on [HttpPost] action. Before, when you didn't have this line, only the following properties were populated:
Item.ItemId
Item.Product.Name
Item.Shipping.Cost
Your model didn't have information for the value of Item.Product.ProductId. This property is int, which means that it was turning to be equal 0 when form was submitted. Inside your _productRepository.UpdateProduct(p); method you are trying to get a product by Id and obviously it cannot find a product with id = 0, which is why this call was returning null resulting in a null reference exception on the next line.

Related

How to find reason "A circular reference was detected while serializing.... 'System.Reflection.RuntimeModule'

I have setup the view MyView.chstml with two submit action. but give me "A circular reference was detected while serializing an object of type 'System.Reflection.RuntimeModule'. How to troubleshoot easily ?
#using (Html.BeginForm("MyView", "Worker", FormMethod.Post, new { enctype = "multipart/form-data", #name = "formWorker" }))
{
#Html.AntiForgeryToken()
<div class="form-horizontal" id="divWork"> ....
<div class="form-group">
<button id="btnSubmit" class="btn btn-success" type="submit" value="Work1">Work1</button>
</div>
<div id="Dynamictable" class="table-responsive hidden" name="dtable">
<table class="table table-bordered" id="dctable" name="dctable"></table>
</div>
<div id="dialogsubmit" title="second submit">
<div id="dialog-content" name="dialog-content" class="form-control hidden"> </div>
</div>
</div>
then in script
(function () {
//strangely the below ajax is never called
$('#formWorker').submit(function () {
debugger;
$.ajax({
url: this.action,
type: this.method,
data: $(this).serialize(),
success: function (result) {
debugger;
bootbox.alert(result);
}
});
// it is important to return false in order to
// cancel the default submission of the form
// and perform the AJAX call
return false;
});
}
$("#btnSubmit").click(function () {
if(document.getElementById('dctable').getElementsByTagName("tr").length < 1)
{
aRow = document.all("dctable").insertRow();
var oCell = aRow.insertCell();
oCell = newRow.insertCell();
oCell.innerHTML = '<input type="submit" name="submit2" value="submit2" />';
**//strangely if i replace the above RHS with below, it act as submit halft submit form (half becuase the FormCollection object in HttpPost method of controller lacks key submit2 and also HttpPostedFileBase object came as null in controller method ,**
*//'<button id="submit2" name="submit2" class="btn submit2" value="submit2" ><i class="fa fa-search"></i> submit2</button>'*
return false;
}
});
</script>
in controller
[HttpGet]
public ActionResult Worker()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Worker(HttpPostedFileBase file, FormCollection form)
{
string value = form["submit2"]; //half submit form as it is null in additon to file is null. if i use commented RHS , all good here, but no option in view to process the output in same view.
IEnumerable<object> data = Enumerable.Empty<object>();
if (value != null) // key doesn't exist
{
//process here and and return json to shown result on same page using popup/alert.
return this.Json(
new
{
Result = data.ToList()
}, JsonRequestBehavior.AllowGet
);
}
return PartialView("anoterhView", data.ToList());
}
A circular reference exception is generally thrown when the JSON serializer initiates a loop that causes effectively an infinite loop of serializing. This happens when you look at the schema of your serializable properties. For example:
Take the two classes below:
public class Node
{
public Node Parent { get; set; }
}
public class Parent : Node
{
public List<Node> Children { get; set; } = new List<Node>();
}
As you can see we have two simple classes, a Node class which has a public property Parent which is type Node. Now if we look at the Parent class it derrives from Node and has the property Children which is a List<Node>. This is where we will start to have circular dependency issues. Consider the following simple method.
public string SerializeJsonObject()
{
var parent = new Parent();
var child = new Node();
child.Parent = parent;
parent.Children.Add(child);
return Json(parent);
}
This method constructs a Parent object parent and then a Node object child. Next we set the Parent property of child to the parent instance. Then we Add the child to the parent's Children list.
Now consider the serialization of parent.
-- Parent Item
-- Parent: null no procesing
-- Children : Serialize each Node object
-- Node 1
-- Parent
-- Parent: null no processing
-- Children:
-- Node 1
-- Parent
--Parent: null no processing
--Children:
-- Node 1
..... continues forever never finishing serializing Node 1
From this we can see our circular dependency is the Parent property however it is only a circular reference because then Parent has a reference to the Node in the Children collection. Therefore the serializer can never finish serializing the object.
Now this isn't limited to lists we can look at a similar example where two classes have a reference to each other and are both serializable. Consider the following class.
public class Node
{
public string Name { get; set; }
public Node Previous { get; set; }
public Node Next { get; set; }
}
This class has a dependency on Node for both the Previous and Next property. Therefore given a method of constructing a small dataset.
public static object SerailizeANode()
{
Node first = null;
Node previous = null;
for(var i = 0; i < 10; i++)
{
var current = new Node();
current.Name = $"Node {i}";
if(previous != null)
{
previous.Next = current;
current.Previous = previous;
}
previous = current;
if (first == null)
first = current;
}
return Json(first);
}
This is really simple but ends up with 10 objects where 1-9 have a dependency on the Next node and objects 2-10 have a dependency to the Previous node. So given the serialization of first
-- first
-- Name: Node 0
-- Previous: Null
-- Next:
-- Name: Node 1
-- Previous
-- Name: Node 0
-- Previous: null
-- Next:
-- Name: Node 1
-- Previous:
-- Name: Node 0
-- Previous: null
-- Next:
--Name: Node 1
continues on forever.
Again as we see by the dependency serialization of the Property (Previous & Next) cause the serializer to hit a circular reference (infinite loop) and throws an exception.
My expectation is there a similar issue in the data that is returning to the browser through the section nicely commented out.
//process here and and return json to shown result on same page using popup/alert.
return this.Json(new
{
Result = data.ToList()
}, JsonRequestBehavior.AllowGet);
If you need more information could you please post the schema of the classes being returned in the commented out section.

Model binder data issue from a EditorTemplate

I'm showing in a List in a Razor view. In it I have several Editor templates that are displayed in the list view. Here is my editor template.
#using Contoso.MvcApplication.Extensions
#model Contoso.MvcApplication.ViewModels.MultipleChoiceQuestionViewModel
<h5>#Model.Question.QuestionText</h5>
<div>
#Html.RadioButtonForSelectList(m => m.Question.SelectedAnswer, Model.AnswerRadioList)
#Html.ValidationMessageFor(m => m.Question.SelectedAnswer)
</div>
The issue is where I set the RadioButtonForSelectList, it's binding so so, because I know at this situation should be inside a for loop like this:
#Html.RadioButtonForSelectList(m => m[i].Question.SelectedAnswer, Model.AnswerRadioList) // the index
But from the Editor template, I have no way to know the index inside a lambda expression.
This is the site where I copied the html extension from:
http://jonlanceley.blogspot.mx/2011/06/mvc3-radiobuttonlist-helper.html
And here is the view model that I'm using
public class MultipleChoiceQuestionViewModel
{
public MultipleChoiceQuestion Question { get; set; }
public List<SelectListItem> AnswerRadioList { get; set; }
}
How do I correctly bind the radioButton?
When I read the tag in code, all the models in my list have the same id: Question.SelectedAnswer. I assume this is wrong, because there should be an indexed ID like so: Question.SelectedAnswer.[INDEX].
UPDATE:
public ActionResult Index(short testId)
{
GenerateQuiz(testId);
StartQuiz();
return View(CreateQuestionViewModel((MultipleChoiceQuestion)CurrentQuestion));
}
[HttpPost]
public ActionResult Index(MultipleChoiceQuestionViewModel q)
{
// Save answer state
((MultipleChoiceQuestion)CurrentQuestion).SelectedAnswer = q.Question.SelectedAnswer;
if (CurrentNumber == Questions.Count - 1)
{
QuizCompleted();
return RedirectToAction("ShowResults");
}
else
{
NextQuestion();
return View(CreateQuestionViewModel((MultipleChoiceQuestion)CurrentQuestion));
}
}
The part of the view that displays the questions should look like this:
#for (int j = 0; j < Model.Questions.Count; j++)
{
<h5>
Model.Questions[j].QuestionText
</h5>
<div>
#Html.RadioButtonForSelectList(m => m.Questions[j].SelectedAnswer, Model.AnswerRadioList)
</div>
}

How to customize the EditorFor CSS with razor

I have this class
public class Contact
{
public int Id { get; set; }
public string ContaSurname { get; set; }
public string ContaFirstname { get; set; }
// and other properties...
}
And I want to create a form that allo me to edit all those fields. So I used this code
<h2>Contact Record</h2>
#Html.EditorFor(c => Model.Contact)
This works fine, but I want to customize how the elements are displayed. For instance I want each field to be displayed in the same line as its label. Because now, the generated html is like this :
<div class="editor-label">
<label for="Contact_ContaId">ContaId</label>
</div>
<div class="editor-field">
<input id="Contact_ContaId" class="text-box single-line" type="text" value="108" name="Contact.ContaId">
</div>
I agree to the solution of jrummell above:
When you use the EditorFor-Extension, you have to write a custom
editor template to describe the visual components.
In some cases, I think it is a bit stiff to use an editor template for
several model properties with the same datatype. In my case, I want to use decimal currency values in my model which should be displayed as a formatted string. I want to style these properties using corresponding CSS classes in my views.
I have seen other implementations, where the HTML-Parameters have been appended to the properties using annotations in the Model. This is bad in my opinion, because view information, like CSS definitions should be set in the view and not in a data model.
Therefore I'm working on another solution:
My model contains a decimal? property, which I want to use as a currency field.
The Problem is, that I want to use the datatype decimal? in the model, but display
the decimal value in the view as formatted string using a format mask (e.g. "42,13 €").
Here is my model definition:
[DataType(DataType.Currency), DisplayFormat(DataFormatString = "{0:C2}", ApplyFormatInEditMode = true)]
public decimal? Price { get; set; }
Format mask 0:C2 formats the decimal with 2 decimal places. The ApplyFormatInEditMode is important,
if you want to use this property to fill a editable textfield in the view. So I set it to true, because in my case I want to put it into a textfield.
Normally you have to use the EditorFor-Extension in the view like this:
<%: Html.EditorFor(x => x.Price) %>
The Problem:
I cannot append CSS classes here, as I can do it using Html.TextBoxFor for example.
To provide own CSS classes (or other HTML attributes, like tabindex, or readonly) with the EditorFor-Extension is to write an custom HTML-Helper,
like Html.CurrencyEditorFor. Here is the implementation:
public static MvcHtmlString CurrencyEditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, Object htmlAttributes)
{
TagBuilder tb = new TagBuilder("input");
// We invoke the original EditorFor-Helper
MvcHtmlString baseHtml = EditorExtensions.EditorFor<TModel, TValue>(html, expression);
// Parse the HTML base string, to refurbish the CSS classes
string basestring = baseHtml.ToHtmlString();
HtmlDocument document = new HtmlDocument();
document.LoadHtml(basestring);
HtmlAttributeCollection originalAttributes = document.DocumentNode.FirstChild.Attributes;
foreach(HtmlAttribute attr in originalAttributes) {
if(attr.Name != "class") {
tb.MergeAttribute(attr.Name, attr.Value);
}
}
// Add the HTML attributes and CSS class from the View
IDictionary<string, object> additionalAttributes = (IDictionary<string, object>) HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
foreach(KeyValuePair<string, object> attribute in additionalAttributes) {
if(attribute.Key == "class") {
tb.AddCssClass(attribute.Value.ToString());
} else {
tb.MergeAttribute(attribute.Key, attribute.Value.ToString());
}
}
return MvcHtmlString.Create(HttpUtility.HtmlDecode(tb.ToString(TagRenderMode.SelfClosing)));
}
The idea is to use the original EditorFor-Extension to produce the HTML-Code and to parse this HTML output string to replace the created
CSS Html-Attribute with our own CSS classes and append other additional HTML attributes. For the HTML parsing, I use the HtmlAgilityPack (use google).
In the View you can use this helper like this (don't forget to put the corresponding namespace into the web.config in your view-directory!):
<%: Html.CurrencyEditorFor(x => x.Price, new { #class = "mypricestyles", #readonly = "readonly", #tabindex = "-1" }) %>
Using this helper, your currency value should be displayed well in the view.
If you want to post your view (form), then normally all model properties will be sent to your controller's action method.
In our case a string formatted decimal value will be submitted, which will be processed by the ASP.NET MVC internal model binding class.
Because this model binder expects a decimal?-value, but gets a string formatted value, an exception will be thrown. So we have to
convert the formatted string back to it's decimal? - representation. Therefore an own ModelBinder-Implementation is necessary, which
converts currency decimal values back to default decimal values ("42,13 €" => "42.13").
Here is an implementation of such a model binder:
public class DecimalModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
object o = null;
decimal value;
var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
var modelState = new ModelState { Value = valueResult };
try {
if(bindingContext.ModelMetadata.DataTypeName == DataType.Currency.ToString()) {
if(decimal.TryParse(valueResult.AttemptedValue, NumberStyles.Currency, null, out value)) {
o = value;
}
} else {
o = Convert.ToDecimal(valueResult.AttemptedValue, CultureInfo.CurrentCulture);
}
} catch(FormatException e) {
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return o;
}
}
The binder has to be registered in the global.asax file of your application:
protected void Application_Start()
{
...
ModelBinders.Binders.Add(typeof(decimal), new DecimalModelBinder());
ModelBinders.Binders.Add(typeof(decimal?), new DecimalModelBinder());
...
}
Maybe the solution will help someone.
Create a partial view called Contact.cshtml with your custom markup in Views/Shared/EditorTemplates. This will override the default editor.
As noted by #smartcavemen, see Brad Wilson's blog for an introduction to templates.

Control isn't getting the Selected value from DropDownListFor

I got the following Model:
public class ViewBloqueioNotaFiscal
{
public ViewComboStatus ComboStatus = new ViewComboStatus();
public class ViewComboStatus
{
public int? IdStatusSelecionado { get; set; }
public IEnumerable<SelectListItem> ComboStatus { get; set; }
}
}
The following controller method:
public ViewBloqueioNotaFiscal.ViewComboStatus geraComboStatus(int? statusSelecionado)
{
ViewBloqueioNotaFiscal.ViewComboStatus combo = new ViewBloqueioNotaFiscal.ViewComboStatus
{
IdStatusSelecionado = statusSelecionado,
ComboStatus = new[]{
new SelectListItem { Value = 1, Text = "Op1"},
new SelectListItem { Value = 2, Text = "Op2"}
}
};
return combo;
}
And my aspx is like:
<%: Html.DropDownListFor(x => x.ComboStatus.IdStatusSelecionado, Model.ComboStatus.ComboStatus) %>
Its getting perfectly displayed for selection but when I submit my form, my post method from controller gets the model perfectly with the values except for this combo that Im recieving null value into the model. As its the first one that I try, I think that something is wrong.
Could you guys check that for me? If you have any better solution for this I d like to know too.
thanks for the help !
You are not binding to the correct property of your view model. You are binding to some complex object (ComboStatus) which doesn't make sense.
You should bind the drop down list to the IdStatusSelecionado property:
<%: Html.DropDownListFor(
x => x.ComboStatus.IdStatusSelecionado,
Model.ComboStatus.ComboStatus
) %>
A strongly typed DropDownListFor helper requires at least 2 things on your view model:
A scalar property (int, decimal, string, ...) which will be used to bind to
A collection of value/text pairs.
If the collection of value/text pairs contains an item whose value is equal to the scalar property you used as first argument, this item will be preselected. For example if you wanted to preselect the second item in your example you would set IdStatusSelecionado=2 on your view model.
Side note: Model.ComboStatus.ComboStatus looks terrible. Please rename.

HTML.DropDownList values from multiple sources?

In ASP.NET MVC, is it possible to fill the list of values of a Html.DropDownList from multiple data sources along with multiple manually entered values?
Basically, I envision it being formated like the below using something along the lines of OPTGROUP:
**Group 1**
Manual Item 1
Manual Item 2
**Group 2**
DS1 Item 1
DS1 Item 2
**Group 3**
DS2 Item 1
DS2 Item 2
I've thought about using a view on the DB and getting the data from that, however, I've not really the faintest how to lay it out like above using helpers and to pass the data to it from multiple sources.
Thanks for any help in advance.
As always start with a model (actually start with a unit test but no time for this here):
public class MyModel
{
public string SelectedItem { get; set; }
public IEnumerable<SelectListItem> Items { get; set; }
}
Then a controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var items1 = new[]
{
new { Value = "1", Text = "Manual Item 1" },
new { Value = "2", Text = "Manual Item 2" },
};
// TODO: Go fetch those from your repo1
var items2 = new[]
{
new { Value = "3", Text = "DS1 Item 1" },
new { Value = "4", Text = "DS1 Item 2" },
};
// TODO: Go fetch those from your repo2
var items3 = new[]
{
new { Value = "5", Text = "DS2 Item 1" },
new { Value = "6", Text = "DS2 Item 2" },
};
var items = items1.Concat(items2).Concat(items3);
var model = new MyModel
{
Items = new SelectList(items, "Value", "Text")
};
return View(model);
}
}
And finally a strongly typed view to the model:
<%# Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MyApp.Models.MyModel>" %>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<%= Html.DropDownListFor(x => x.SelectedItem, Model.Items) %>
</asp:Content>
You will probably define an intermediary type to avoid the anonymous types that I've used for brevity.
Remark: If your original question was about using an OPTGROUP then ignore my answer and make your intention clear so that you can get a more adapted answer.
It seems it would be easier for you to write your own helper. The basic syntax to do that is this:
// The class can be named anything, but must be static and accessible
public static class HtmlHelperExtensions
{
// The method name is what you want to call on Html,
// in this case Html.CoolSelectList(arguments...)
//
// The method has to be static, and the first argument should be of the type
// you're extending (in this case HtmlHelper, which is the type of the
// Html property on your view). The first argument must be prefixed with the
// "this" keyword, to indicate it's an extension method.
//
// All the following arguments will be arguments that you supply when calling
public static string CoolSelectList(this HtmlHelper helper,
IEnumerable<IEnumerable<CoolThingThatWeMakeAListOf>> groups)
{
// I chose an argument of type IEnumerable<IEnumerable<T>>, since that
// allows you to create each group of item on its own (i.e. get them from
// various data sources) and then add all of them to a list of groups
// that you supply as argument. It is then easy to keep track of which
// items belong to which groups, etc.
// Returned from the extension method is a string you have built, that
// constitutes the html you want to output on your view. I usually use
// the TagBuilder class to build the html.
return "this is what will be on the page";
}
}
Many solutions exist for you problem. One would be the one that Tomas described, another is a Controller Action that returns PartialView, which contains the code to render the input and option tags, another solution would be to have the Controller Action populate the ViewData with a SelectList or have the SelectList as a strong type for your View/ViewUserControl (Partial).

Resources