ASP.Net Core 2.0 MVC dynamic model updating - asp.net-core-mvc

I'm trying to build a dynamic view, where I can pass a list of properties that need to be filled in by the user.
The collection of properties is dynamic, so I can't build the view that displays specific properties.
I've been able to display the property names and their initial values, and the user can change the values on the screen, but those updated values don't make it to the controller action that would update the model.
I've tried using a dynamic model, as well as a list of key/value pairs.
I'm thinking that it has something to do with the over-posting protection. Since the properties are dynamic, I can't list them in the Bind attribute in the update action.
Here's the controller action methods:
public IActionResult Test()
{
dynamic testObj = new ExpandoObject();
testObj.IntProperty = 100;
testObj.StringProperty = "A String Value";
return View(testObj);
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Test(ExpandoObject model)
{
return Ok();
}
Here's the view:
#model dynamic
#{
ViewData["Title"] = "Test";
}
<form asp-action="Test" method="post">
<div class="form-horizontal">
#foreach (var propertyName in ((System.Collections.Generic.IDictionary<string, object>)Model).Keys)
{
<div class="form-group">
<label class="col-md-2 control-label">#propertyName</label>
<div class="col-md-10">
#Html.TextBox(propertyName, ((System.Collections.Generic.IDictionary<string, object>)Model)[propertyName])
</div>
</div>
}
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
</form>
This is a new application, and I'm doing code-first - so the model can be changed somewhat. All I really need to do is to be able to have different properties that can be updated.
Thanks in advance.

I recommend that you do not rely on the IModelBinder for this purpose at all and why I recommend this is because the form data that is passed between the controller and view is dynamic in terms of structure. A better, yet more troublesome, solution would be to get the form data directly out of the HttpContext.Request.Form. This type has an indexer which allows you to access the posted values by their names. For example
var name = HttpContext.Request.Form["name"].FirstOrDefault();
The .FirstOrDefault() (or SingleOrDefault(), this throws an exception when finding more than a value that meets a condition in a collection) is called assuming that there would be a single value (or a single value that meets a condition) for the "Name" input. However, when you have an array of those, you can get the values either in a foreach loop, or using linq methods, or directly by an index. For example:
var name = HttpContext.Request.Form["name"].FirstOrDefault(x=> x.ToString().StartsWith("x"));
var name = HttpContext.Request.Form["name"][0];

Related

Partial view in NET 6

i am lost with NET6.
I created this partial view _MenuNav.cshtml :
#model IEnumerable<CateringMilano.Models.Menu>
#foreach (var item in Model)
{
<div>
<a alt="#item.MenuTitle)">#item.MenuName</a>
<b>#item.MenuTitle</b>
</div>
}
and the cod in my controller is :
// GET: /Menus/_MenuNav/
public ActionResult _MenuNav(string menuPosition)
{
// Get my db
var model = _context.Menu.Include(m => m.ChildMenuSub) as IQueryable<Menu>;
model = model.Where(p => p.MenuPosition == menuPosition);
// Send your collection of Mreations to the View
return PartialView(model);
}
and in the last project with net 4 i use to write the following code in the principal view in order to call my partial view :
#Html.Action("_MenuNav", "Menus", new { menuPosition = "Menu" })
but it looks like it does not work anymore this with NET6
Do you know how to rewrite my code in order to get the same result?
you must be mixing view components with partial views. Partial view were always used like this
<div id="partialName">
<partial name="_PartialName" />
</div>
with model
<partial name="_PartialName" model="Model.MyInfo" />
or for async
<div id="partialName">
#await Html.PartialAsync("_PartialName")
</div>
and the most popular way to update parital view is using ajax
And you can check my answer about viewcomponents here
https://stackoverflow.com/a/69698058/11392290

ASP.NET Core Boolean property binding on form POST does not work as expected

I have got a bare-bones ASP.NET Core MVC application generated off of the MVC project template. The HomeController has been modified to expose two Index method overloads – one for GET and one for POST request, both of which render the same Index.cshtml view with a simple BooleanInputsViewModel:
public class BooleanInputsViewModel
{
public bool IsImportant { get; set; }
public bool IsActive { get; set; }
public List<string> Messages { get; } = new List<string>();
}
The Index.cshtml view looks like this:
#model AspNetCoreBooleanInputs.Models.BooleanInputsViewModel
<h2>
#nameof(this.Model.IsImportant) = #this.Model.IsImportant.ToString()
<br />
#nameof(this.Model.IsActive) = #this.Model.IsActive.ToString()
</h2>
<form class="form-horizontal" method="post">
<div class="col-md-12">
<input asp-for="IsImportant" />
<label asp-for="IsImportant">
</label>
<input asp-for="IsActive" />
<label asp-for="IsActive">
</label>
</div>
<div class="col-md-12">
<ul>
#foreach(string message in this.Model.Messages)
{
<li>#message</li>
}
</ul>
</div>
<div class="col-md-12">
<button type="submit">Submit</button>
</div>
</form>
Finally, the HomeController Index methods are implemented like this:
public IActionResult Index()
{
var model = new BooleanInputsViewModel();
model.Messages.Add($"GET values: {nameof(model.IsImportant)} = {model.IsImportant}, {nameof(model.IsActive)} = {model.IsActive}");
return View(model);
}
[HttpPost]
public IActionResult Index(BooleanInputsViewModel model)
{
model.Messages.Add($"POST values: {nameof(model.IsImportant)} = {model.IsImportant}, {nameof(model.IsActive)} = {model.IsActive}");
model.IsActive = !model.IsActive;
model.IsImportant = !model.IsImportant;
model.Messages.Add($"Negated POST values: {nameof(model.IsImportant)} = {model.IsImportant}, {nameof(model.IsActive)} = {model.IsActive}");
return this.View(model);
}
The POST handler negates the two model properties and passes the modified model back to the view. However, the negated values are not reflected in the rendered form as it always renders the originally POST-ed values. To me this looks like a bug. Do I miss something obvious?
The complete ASP.NET Core project is posted here - https://github.com/PaloMraz/AspNetCoreBooleanInputs.
Edit based on #Chris Platt's answer below:
Hi Chris, thank you for the prompt answer. I have verified that using the ModelState dictionary as you suggested works as expected, e.g.:
[HttpPost]
public IActionResult Index(BooleanInputsViewModel model)
{
model.Messages.Add($"POST values: {nameof(model.IsImportant)} = {model.IsImportant}, {nameof(model.IsActive)} = {model.IsActive}");
// This does NOT work:
//model.IsActive = !model.IsActive;
//model.IsImportant = !model.IsImportant;
// This works:
this.ModelState[nameof(model.IsActive)].RawValue = !model.IsActive;
this.ModelState[nameof(model.IsImportant)].RawValue = !model.IsImportant;
model.Messages.Add($"Negated POST values: {nameof(model.IsImportant)} = {model.IsImportant}, {nameof(model.IsActive)} = {model.IsActive}");
return this.View(model);
}
However, I still find it a very cumbersome behavior, because the model has already been bound once the Index method gets called. Why is the binding occurring again in the call to the View method, effectively ignoring the passed in model instance? This does not look right to me, sorry.
Besides, can you tell me please where did you get the information about the ModelState dictionary composition? The official documentation at https://learn.microsoft.com/en-us/aspnet/core/mvc/models/model-binding does not mention ViewData/ViewBag as sources; only form values, route values and query string...
The values your form fields are set to come from ModelState, which is composed of values from Request, ViewData/ViewBag, and finally Model. Importantly, Model is a last resort, so once you've done a post, the posted value (in Request) will be what the field is set to, regardless of any changes you make to Model.
I haven't tried doing this in ASP.NET Core, but you should be able to set ModelState["IsActive"].RawValue and ModelState["IsImportant"].RawValue instead. Assuming you can change the value in ModelState, then it will display as you want on your view.

passing data from view to controller on asp.net mvc 3

i have problem to pass data from view to controller , i have view that is strongly typed with my viewmodel "TimeLineModel", in the first i passed to this view my viewmodel from action on my controller
public ActionResult confirmation(long socialbuzzCompaignId)
{
return View(new TimeLineModel() { socialBuzzCompaignId = socialbuzzCompaignId, BuzzMessages = model });
}
with this i can get info from my action and display it on view , but i have other action POST which i won't get my view model to do some traitement
[HttpPost]
public ActionResult confirmation(TimeLineModel model)
{
}
i can get some propretie of the model but in others no , for example i can get the properti "socialBuzzCompaignId" of model , but other propertie like "IEnumerable BuzzMessages" i can't get it , i dont now why !!
this is the content of my view
#model Maya.Web.Models.TimeLineModel
#{
ViewBag.Title = "confirmation";
}
#using (Html.BeginForm())
{
<h2>confirmation</h2>
<fieldset>
#foreach (var msg in Model.BuzzMessages)
{
<div class="editor-label">
#msg.LongMessage
</div>
<br />
}
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
You need to include BuzzMessages properties within a form element. Since it's not editable, you'd probably want to use hiddens. There are two ways to do this. Easiest is instead of doing a foreach loop, do a for loop and insert them by index.
#for (int i =0; i<Model.BuzzMessages.Count(); i++v)
{
<div class="editor-label">
#Model.BuzzMessages[i].LongMessage
#Html.HiddenFor(m => m.BuzzMessages[i].LongMessage);
</div>
<br />
}
but to do this you'd need to use an IList instead of an IEnumerable in your view model to access by index.
Alternatively, you could create an Editor Template named after your BuzzMessages class (whatever its name is).
#model BuzzMessagesClass
#Html.HiddenFor(m => m.LongMessages)
<!-- Include other properties here if any -->
and then in your main page
#Html.EditorFor(m => m.BuzzMessages)
Check out http://coding-in.net/asp-net-mvc-3-how-to-use-editortemplates/ or search stack overflow if the details of editor templates confuse you.
Just like any HTML POST method, you have to get the data back to the Controller somehow. Just simply "showing" the data on the page doesn't rebind it.
You have to put the data in an input (or a control that will post back) to the appropriate model property name.
So, if you have a model property with name FirstName and you want this data to be rebound to the model on POST, you have to supply it back to the model by placing an "input hidden" (or similar control that postbacks) with the ID of FirstName will rebind that property to the model on POST.
Hope that explains it.
#foreach (var msg in Model.BuzzMessages)
{
<div class="editor-label">
#msg.LongMessage
<input type="hidden" name="BuzzMessages.LongMessage" value="#msg.LongMessage" />
</div>
}
It will post array of LongMessages. Get values like this:
[HttpPost]
public ActionResult confirmation(TimeLineModel model, FormCollection collection)
{
var longMessages = collection["BuzzMessages.LongMessage"];
}

Setting form method to get without specifying controller and action

I have a partial view which I need to re-use:
div class="selectDate">
#using (Html.BeginForm("ViewTransactionLog", "Profile", FormMethod.Get))
{
<div class="selectDateLabel">Date:</div>
<div>
#Html.TextBox("start", range.Start, new { #class = "pickDate" }) to #Html.TextBox("end", range.End, new { #class = "pickDate" })
</div>
<div>
<input type="submit" value="Go" />
</div>
}
</div>
This is the code for picking 2 dates. As the data is lightweight, I wish to pass it through the Get method. I also wish to generalize it and put it into its own cshtml; however, Html.BeginForm expects the controller name and action name to be given if I wish to use the Get method. Is there anyway to avoid this so I could just move the code into a partial view of its own?
Assuming you want the form to post back to the current controller and action, you should be able to use an extension method:
public static MvcForm BeginForm<TModel>(
this HtmlHelper<TModel> html,
FormMethod formMethod)
{
string controller = (string)html.ViewContext.RouteData.Values["controller"];
string action = (string)html.ViewContext.RouteData.Values["action"];
return html.BeginForm(action, controller, formMethod);
}

ViewModel has no properties set in Model using HttpPost Create method

I have a simple MVC3 app with an EF4 Model
Log
.Name
.CreatedDate
.LogTypeId
LogTypes
.Id
.Description
and a ViewModel
LogViewModel
Log MyLog
List<SelectListItem> Options
LogViewModel(){
Log = new Log();
}
This displays in my view correctly and I can edit/update the values, display the drop down list and set the name to "MyTestValue".
However, in my controller's HttpPost Create method the properties for logVm.Log are not set?
[HttpPost]
public ActionResult Create(LogViewModel logVm){
logVm.Log.Name == "MyTestvalue"; //false - in fact its null
}
What am I doing wrong?
That's probably because in your edit form you don't have corresponding values. So if yuor view is strongly typed to LogViewModel the form input names must be appropriately named:
#model LogViewModel
#using (Html.BeginForm())
{
<div>
#Html.LabelFor(x => x.Log.Name)
#Html.EditorFor(x => x.Log.Name)
</div>
<div>
#Html.LabelFor(x => x.Log.SomeOtherProperty)
#Html.EditorFor(x => x.Log.SomeOtherProperty)
</div>
...
<input type="submit" value="OK" />
}
sop that when the form is submitted the POSTed values look like this:
Log.Name=foo&Log.SomeOtherProperty=bar
Now the default model binder will be able to successfully bind your view model. Also make sure that the properties you are trying to assign are public and that have a setter.
The controller method should have a property named model
[HttpPost]
public ActionResult Create(LogViewModel **model**){
**model**.Log.Name == "MyTestvalue"; //true }

Resources