Bind a List of object using Razor page - c#

I have razor pages .
I have prepared in a handler a list of(visitorTypes)
I only want to bind them . there is something that i have missed , but i dont know what
Here is my c# code
[ModelBinder(Name ="Visitors")]
public ICollection<VisitorType> VisitorTypes { get; set; }
public IActionResult OnGetListOfVisitorTypeAsync()
{
VisitorTypes = _db.VisitorTypes.ToList();
return RedirectToPagePermanent("/Visitors",VisitorTypes);
}
And here is my razor page
<div class="container">
<form method="get" asp-page-handler="ListOfVisitorType" >
#foreach (var item in Model.VisitorTypes)
{
<label>#item.VisitorTypeName.ToString() </label>
}
</form>
</div>
can someone please explain what im doing wrong
(I have tried to return the list , i have tried to make it a void method , but none of them works with me )
Here Is The modal
private string _VisitorTypeName { get; set; }
public string VisitorTypeName { get {return _VisitorTypeName; } set { _VisitorTypeName = value; } }
ICollection<Visitor> Visitors { get; set; }

What if you generate your data when your View get loaded, something like this:
public IActionResult Visitors()
{
VisitorTypes = _db.VisitorTypes.ToList();
return View(VisitorTypes);
}

You may have passed wrong model with "#model ?" in razor page.

This is pretty Simple
First you need to know how actually MVC works and what it's:
MVC is a architectural pattern , we keep all things loosely coupled in terms of MODEL, View and Controller
So it's doesn't means that cannot write code directly in "Razor View",
For example how we write JavaScript directly in HTML or View. for decoupling purpose we create .JS file to separate script code from View(HTML).
Now comes to actual work
First before accessing List(VisitorTypes) it must be declare and initialize with value in the razorview itself
#{
List<VisitorTypes> listVisitorTypes = _db.VisitorTypes.ToList();
}
And finally render the list:
#foreach (var item in listVisitorTypes )
{
#item.VisitorTypeName.ToString()
}

Related

Multiple view components in .net core razor page not binding correctly

I am creating a .net core 5 web application using razor pages and am struggling with binding view components that I have created to my page -- IF I have multiple of the same view component on the page.
The below works perfectly:
MyPage.cshtml:
#page
#model MyPageModel
<form id="f1" method="post" data-ajax="true" data-ajax-method="post">
<vc:my-example composite="Model.MyViewComposite1" />
</form>
MyPage.cshtml.cs
[BindProperties]
public class MyPageModel : PageModel
{
public MyViewComposite MyViewComposite1 { get; set; }
public void OnGet()
{
MyViewComposite1 = new MyViewComposite() { Action = 1 };
}
public async Task<IActionResult> OnPostAsync()
{
// checking on the values of MyViewComposite1 here, all looks good...
// ...
return null;
}
}
MyExampleViewComponent.cs:
public class MyExampleViewComponent : ViewComponent
{
public MyExampleViewComponent() { }
public IViewComponentResult Invoke(MyViewComposite composite)
{
return View("Default", composite);
}
}
Default.cshtml (my view component):
#model MyViewComposite
<select asp-for="Action">
<option value="1">option1</option>
<option value="2">option2</option>
<option value="3">option3</option>
</select>
MyViewComposite.cs
public class MyViewComposite
{
public MyViewComposite() {}
public int Action { get; set; }
}
So up to this point, everything is working great. I have a dropdown, and if I change that dropdown and inspect the value of this.MyViewComposite1 in my OnPostAsync() method, it changes to match what I select.
However, I now want to have MULTIPLE of the same view component on the page. Meaning I now have this:
MyPage.cshtml:
<form id="f1" method="post" data-ajax="true" data-ajax-method="post">
<vc:my-example composite="Model.MyViewComposite1" />
<vc:my-example composite="Model.MyViewComposite2" />
<vc:my-example composite="Model.MyViewComposite3" />
</form>
MyPage.cshtml:
[BindProperties]
public class MyPageModel : PageModel
{
public MyViewComposite MyViewComposite1 { get; set; }
public MyViewComposite MyViewComposite2 { get; set; }
public MyViewComposite MyViewComposite3 { get; set; }
public void OnGet()
{
MyViewComposite1 = new MyViewComposite() { Action = 1 };
MyViewComposite2 = new MyViewComposite() { Action = 1 };
MyViewComposite3 = new MyViewComposite() { Action = 2 };
}
public async Task<IActionResult> OnPostAsync()
{
// checking on the values of the above ViewComposite items here...
// Houston, we have a problem...
// ...
return null;
}
}
I now have three downdowns showing on the page as I would expect, and those three dropdowns are all populated correctly when the page loads. So far so good!
But let's say that I selection "option3" in the first dropdown and submit the form. All of my ViewComposites (MyViewComposite1, MyViewComposite2 and MyViewComposite3) ALL show the same value for Action, even if the dropdowns all have different options selected.
I believe that I see WHY this is happening when I inspect the controls using dev tools:
<select name="Action">...</select>
<select name="Action">...</select>
<select name="Action">...</select>
As you can see, what is rendered is three identical options, all with the same name of "Action". I had hoped that giving them different ids would perhaps help, but that didn't make a difference:
<select name="Action" id="action1">...</select>
<select name="Action" id="action2">...</select>
<select name="Action" id="action3">...</select>
This is obviously a slimmed down version of what I am trying to do, as the view components have a lot more in them than a single dropdown, but this illustrates the problem that I am having...
Is there something I am missing to make this work?
Any help would be greatly appreciated!
The HTML output shows clearly that all the selects have the same name of Action which will cause the issue you are encountering. Each ViewComponent has no knowledge about its parent view model (of the parent view in which it's used). So basically you need somehow to pass that prefix info to each ViewComponent and customize the way name attribute is rendered (by default, it's affected only by using asp-for).
To pass the prefix path, we can take advantage of using ModelExpression for your ViewComponent's parameters. By using that, you can extract both the model value & the path. The prefix path can be shared in the scope of each ViewComponent only by using its ViewData. We need a custom TagHelper to target all elements having asp-for and modify the name attribute by prefixing it with the prefix shared through ViewData.
That will help the final named elements have their name generated correctly, so the model binding will work correctly after all.
Here is the detailed code:
[HtmlTargetElement(Attributes = "asp-for")]
public class NamedElementTagHelper : TagHelper
{
[ViewContext]
[HtmlAttributeNotBound]
public ViewContext ViewContext { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
//get the name-prefix shared through ViewData
//NOTE: this ViewData is specific to each ViewComponent
if(ViewContext.ViewData.TryGetValue("name-prefix", out var namePrefix) &&
!string.IsNullOrEmpty(namePrefix?.ToString()) &&
output.Attributes.TryGetAttribute("name", out var attrValue))
{
//format the new name with prefix
//and set back to the name attribute
var prefixedName = $"{namePrefix}.{attrValue.Value}";
output.Attributes.SetAttribute("name", prefixedName);
}
}
}
You need to modify your ViewComponent to something like this:
public class MyExampleViewComponent : ViewComponent
{
public MyExampleViewComponent() { }
public IViewComponentResult Invoke(ModelExpression composite)
{
if(composite?.Name != null){
//share the name-prefix info through the scope of the current ViewComponent
ViewData["name-prefix"] = composite.Name;
}
return View("Default", composite?.Model);
}
}
Now use it using tag helper syntax (note: the solution here is convenient only when using tag helper syntax with vc:xxx tag helper, the other way of using IViewComponentHelper may require more code to help pass the ModelExpression):
<form id="f1" method="post" data-ajax="true" data-ajax-method="post">
<vc:my-example composite="MyViewComposite1" />
<vc:my-example composite="MyViewComposite2" />
<vc:my-example composite="MyViewComposite3" />
</form>
Note about the change to composite="MyViewComposite1", as before you have composite="Model.MyViewComposite1". That's because the new composite parameter now requires a ModelExpression, not a simple value.
With this solution, now your selects should be rendered like this:
<select name="MyViewComposite1.Action">...</select>
<select name="MyViewComposite2.Action">...</select>
<select name="MyViewComposite3.Action">...</select>
And then the model binding should work correctly.
PS:
Final note about using a custom tag helper (you can search for more), without doing anything, the custom tag helper NamedElementTagHelper won't work. You need to add the tag helper at best in the file _ViewImports.cshtml closest to the scope of where you use it (here your ViewComponent's view files):
#addTagHelper *, [your assembly fullname without quotes]
To confirm that the tag helper NamedElementTagHelper works, you can set a breakpoint in its Process method before running the page containing any elements with asp-for. The code should hit in there if it's working.
UPDATE:
Borrowed from #(Shervin Ivari) about the using of ViewData.TemplateInfo.HtmlFieldPrefix, we can have a much simpler solution and don't need the custom tag helper NamedElementTagHelper at all (although in a more complicated scenario, that solution of using a custom tag helper may be more powerful). So here you don't need that NamedElementTagHelper and update your ViewComponent to this:
public class MyExampleViewComponent : ViewComponent
{
public MyExampleViewComponent() { }
public IViewComponentResult Invoke(ModelExpression composite)
{
if(composite?.Name != null){
ViewData.TemplateInfo.HtmlFieldPrefix = composite.Name;
}
return View("Default", composite?.Model);
}
}
Each component only binds data, base on the defined model so you always have the same name fields in the result. In razor you can pass viewdata to the component.
you should create custom viewdata for your components.
#{
var myViewComposite1VD = new ViewDataDictionary(ViewData);
myViewComposite1VD.TemplateInfo.HtmlFieldPrefix = "MyViewComposite1";
var myViewComposite2VD = new ViewDataDictionary(ViewData);
myViewComposite2VD.TemplateInfo.HtmlFieldPrefix = "MyViewComposite2";
var myViewComposite3VD = new ViewDataDictionary(ViewData);
myViewComposite3VD.TemplateInfo.HtmlFieldPrefix = "MyViewComposite3";
}
<form id="f1" method="post" data-ajax="true" data-ajax-method="post">
<vc:my-example composite="MyViewComposite1" view-data="myViewComposite1VD " />
<vc:my-example composite="MyViewComposite2" view-data="myViewComposite2VD"/>
<vc:my-example composite="MyViewComposite3" view-data="myViewComposite3VD "/>
</form>
As you see you can change the bindings using TemplateInfo.HtmlFieldPrefix

editing a model in more than one view

My target is, to modify a model in more than one view.
Since sometimes my models have many properties I want to modify them in more than one view. Something like:
first page edits 2 properties, second page edits 3 other properties,...
the model looks like this:
public class LoadViewModel
{
public int CurrentPage { get; set; } = -1;
public PageViewModel PageViewModel { get; set; }
}
public class PageViewModel
{
public string Param1 { get; set; }
public string Param2 { get; set; }
public int Param3 { get; set; }
}
my view on the Index-page looks like this:
#model LoadViewModel
#using(Ajax.BeginForm("Load", "Home", new AjaxOptions {UpdateTargetId = "page"}, new {lvm = Model}))
{
<div id="page"></div>
<input type="submit"/>
}
and this is my action:
public ActionResult Load(LoadViewModel lvm = null)
{
if (lvm == null) lvm = new LoadViewModel();
lvm.CurrentPage += 1;
TempData["CurrentPage"] = TempData["CurrentPage"] == null ? 0 : (int)TempData["CurrentPage"] + 1;
if (!partialViewDict.ContainsKey((int) TempData["CurrentPage"]))
TempData["CurrentPage"] = 0;
return PartialView(partialViewDict[(int)TempData["CurrentPage"]], lvm);
}
the pages are just partials that are mapped:
private Dictionary<int, string> partialViewDict = new Dictionary<int, string>
{
{0, "Pages/_Page1"},
{1, "Pages/_Page2"},
{2, "Pages/_Page3"},
};
and designed like this:
#using WebApplication1.Controllers
#model LoadViewModel
#{
TempData["CurrentPage"] = 0;
}
#Html.DisplayNameFor(m => m.PageViewModel.Param1)
#Html.EditorFor(m => m.PageViewModel.Param1)
this is working. When switching to Page2 the model is correctly set, but when hitting the submit the value of Param1 (that I set in Page1) is resetted to null and only the values I set in the current partial are correct.
This is Page2:
#using WebApplication1.Controllers
#model LoadViewModel
#{
TempData["CurrentPage"] = 1;
}
#Html.DisplayNameFor(m => m.PageViewModel.Param2)
#Html.EditorFor(m => m.PageViewModel.Param2)
When I add a #Html.HiddenFor(m => m.PageViewModel.Param1) into the partial, the value is still set. But I don't want the values to be resetted. I don't want to add an #Html.HiddenFor for all properties a set in a previous view. How can I prevent that the values are resetted when hitting submit without adding #Html.HiddenFor for all not listed attributes? Or is there any other possibility to catch my target?
There's two pieces to this. First, the post itself, and getting that to validate. For that, each step should have its own view model, containing only the properties it's supposed to modify. This allows you to add all the validation you need without causing other steps to fail. In the end, you'll combine the data from all of these into your entity class or whatever.
Which brings us to the second piece. You need some way of persisting the data from each step. The only data that will exist after a POST is the data that was posted and anything in the session (which includes TempData). You could always create a bunch of hidden fields to store the data from the previous steps, but that can get a little arduous. Most likely, you'll just want to use the session.
TempData is basically a specialized instance of Session, so which you use doesn't really matter. With TempData, you'll need to remember call TempData.Keep() for each of the keys you've set for each step or you'll lose the previous steps on the next request. Session will keep them around for the life of the session, but you should remember to remove the keys at the end with Session.Remove().
Do you use #using (Html.BeginForm()) in your .cshtml?
Unfortunately this is MVC. MVC is stateless, which means if you don't render it then you loose it :(
If you use model binding and scaffolding, then you can save some time and work but at the end it will be the same solution.

How can I send DropDownList's SelectedValue to the Controller from View with BeginForm?

How can I send DropDownList's SelectedValue to the Controller from View with BeginForm?
Here's my code:
#using (Html.BeginForm(new { newvalue=ddl.SelectedValue}))
{
#Html.DropDownList("categories",
(List<SelectListItem>)ViewData["categories"],
new { onchange = "this.form.submit()", id = "ddl" })
Do not use ViewData or ViewBag in place of your model. It's sloppy, prone to error and just an unorganized way of giving your view data.
{ newvalue=ddl.SelectedValue} is going to do nothing for you when placed on the form itself. You need to understand that everything you're writing is evaulated on the server before being sent down the client. So if newvalue resolves to 1 it will continue to stay 1 forever unless you have javascript that changes it on the clientside (which you're not doing and you shouldn't be doing).
First you need a model:
public class CategoryModel()
{
public IEnumberable<SelectListItem> CategoriesList {get;set;}
public int SelectedCategoryId {get;set;}
}
Controller
public class CategoryController()
{
public ActionResult Index()
{
var model = new CategoryModel();
model.CategoriesList = new List<SelectListItem>{...};
return View(model);
}
public ActionResult SaveCategory(CategoryModel model)
{
model.SelectedCategoryId
...
}
}
View
#model CategoryModel
#using(Html.BeginForm("SaveCategory","Category"))
{
#Html.DropDownListFor(x=> x.SelectedCategoryId, Model.CategoriesList)
<button type="submit">Submit</button>
}
What's happening here is SelectList is being populated from the IEnumerable and it's form name is SelectedCategoryId, that's what is posed back to the server.
I'm not sure where your knowledge of http and html ends, but you should not be using any framework until you understand how http and html work and then what these helpers such as begin form and Html.DropDownList are actually doing for you. Understand how the screw works before you try to use the screw driver.

Handling nested collections with mvc4 view to controller

I've been struggling for a few hours on a concept that I feel should be simple. I have a Model that is essentially a Quiz with a Collection of Questions and that collection has a collection of Answers. Here is a example of my model (simplified):
public class QuizModel
{
public List<Question> Questions { get; set; }
}
public class Question
{
public string TheQuestion { get; set; }
public List<Answer> Answers { get; set; }
}
public class Answer
{
public string Value { get; set; }
}
And the troublesome part, my view:
#using (Html.BeginForm("SubmitQuiz", "Quiz", FormMethod.Post, new { role = "form" }))
{
<ol>
#{
#Html.Hidden("Id", Model.Id, new { Id="pQuizModel"})
for (int vQIndex = 0; vQIndex < Model.Questions.Count; vQIndex++)
{
<li>
#Model.Questions.ElementAt(vQIndex).Question
<ul class="list-unstyled">
#{
for (int vAIndex = 0; vAIndex < Model.Questions.ElementAt(vQIndex).Answers.Count; vAIndex++)
{
<li>#Html.RadioButtonFor(pModel => pModel.Questions.ElementAt(vQIndex).SelectedAnswer, Model.Questions.ElementAt(vQIndex).Answers.ElementAt(vAIndex).Value) #Model.Questions.ElementAt(vQIndex).Answers.ElementAt(vAIndex).Value</li>
//<li>#Html.RadioButton(Model.Questions.ElementAt(vQIndex).Id.ToString() + ":" + Model.Questions.ElementAt(vQIndex).Answers.ElementAt(vAIndex).Id, Model.Questions.ElementAt(vQIndex).Answers.ElementAt(vAIndex).Id)
// #Model.Questions.ElementAt(vQIndex).Answers.ElementAt(vAIndex).Value</li>
}
}
</ul>
</li>
}
}
</ol>
<button type="submit" class="btn btn-default">Submit</button>
}
My controller, just trying to debug it and make sure the Model is filled out properly:
public ActionResult SubmitQuiz(QuizModel pQuizModel)
{
return View();
}
I've literally tried a ton of different suggestions on my view. It's pretty easy to bind directly to a single value in a Model, I can even get my pQuizModel to have the correct Hidden piece you see in the View. But nothing else in the Model gets populated and I can't figure out why.\
Edit: To clarify my problem, the view is good but the controller does not receive any values in the pQuizModel parameter. I don't have the binding setup properly, need some help there.
The problem is not with the Razor rendering of your initial page, it's with the model binder on the resulting POST (after they push the button). The model binder makes certain assumptions about how to map ids into nested items. Because you're not following the convention as you form your Razor code, it isn't filling the method's input properties. See http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx for a similar example.

Using ASP.NET MVC, how to best avoid writing both the Add View and Edit View?

The Add view and the Edit view are often incredibly similar that it is unwarranted to write 2 views. As the app evolves you would be making the same changes to both.
However, there are usually subtle differences. For instance, a field might be read-only once it's been added, and if that field is a DropDownList you no longer need that List in the ViewData.
So, should I create a view data class which contains all the information for both views, where, depending on the operation you're performing, certain properties will be null?
Should I include the operation in the view data as an enum?
Should I surround all the subtle differences with <% if( ViewData.Model.Op == Ops.Editing ) { %> ?
Or is there a better way?
It's pretty easy really. Let's assume you're editing a blog post.
Here's your 2 actions for new/edit:
public class BlogController : Controller
{
public ActionResult New()
{
var post = new Post();
return View("Edit", post);
}
public ActionResult Edit(int id)
{
var post = _repository.Get(id);
return View(post);
}
....
}
And here's the view:
<% using(Html.Form("save")) { %>
<%= Html.Hidden("Id") %>
<label for="Title">Title</label>
<%= Html.TextBox("Title") %>
<label for="Body">Body</label>
<%= Html.TextArea("Body") %>
<%= Html.Submit("Submit") %>
<% } %>
And here's the Save action that the view submits to:
public ActionResult Save(int id, string title, string body)
{
var post = id == 0 ? new Post() : _repository.Get(id);
post.Title = title;
post.Body = body;
_repository.Save(post);
return RedirectToAction("list");
}
I don't like the Views to become too complex, and so far I have tended to have separate views for Edit and Add. I use a user control to store the common elements to avoid repetition. Both of the views will be centered around the same ViewData, and I have a marker on my data to say whether the object is new or an existing object.
This isn't any more elegant than what you have stipulated, so I wonder if any of the Django or Rails guys can provide any input.
I love asp.net mvc but it is still maturing, and still needs more sugar adding to take away some of the friction of creating websites.
I personally just prefer to use the if/else right there in the view. It helps me see everything going on in view at once.
If you want to avoid the tag soup though, I would suggest creating a helper method.
<%= Helper.ProfessionField() %>
string ProfessionField()
{
if(IsNewItem) { return /* some drop down code */ }
else { return "<p>" + _profession+ "</p>"; }
}
You can specify a CustomViewData class and pass the parameters here.
public class MyViewData {
public bool IsReadOnly { get; set; }
public ModelObject MyObject { get; set; }
}
And both views should implement this ViewData.
As a result you can use provided IsReadOnly property to manage the UserControl result.
As the controller uses this, you can unit test it and your views doesn't have implementation, so you can respect the MVC principles.

Categories

Resources