I am trying out the .net Core ViewComponents instead of partial views. Here is my scenario.
I have a View that contains a few kendo listbox controls. When an item in any of the listbox is clicked, I need to invoke a webcomponent with the selected item id in the listbox. I also need to keep the ViewComponents hidden until the user clicks on a listbox item. Each listbox has it's own ViewComponent because the control in the viewcomponent is different for each listbox. So, I will have to hide some viewcomponents and show only the relevant component for the listbox. I have no clue how to pass the item id in any listbox when selected, to the Invoke method.
Here is my main view (non essential code removed)
<div style="height:25%; width:100%; background-color:antiquewhite;">
<b>Sections:</b>
#(Html.Kendo().ListBox()
.Name( "SectionListBox" )
.Selectable( ListBoxSelectable.Single )
.DataTextField( "Name" )
.DataValueField( "id" )
//.DataSource( ds => ds.Read( r => r.Action( "GetAllPageSectionsAsync", "Home" ) ) )
.Events( e => e.Change( "onSectionSelected" ) )
.HtmlAttributes( new { style = "width:95%;height:85%;" } )
)
</div>
<div style="height:25%; width:100%; background-color:cornsilk;">
<b>Fields:</b>
#(Html.Kendo().ListBox()
.Name( "FieldListBox" )
.Selectable( ListBoxSelectable.Single )
.DataTextField( "Name" )
.DataValueField( "id" )
//.DataSource( ds => ds.Read( r => r.Action( "GetAllFieldsAsync", "Home" ) ) )
.HtmlAttributes( new { style = "width:95%;height:85%;" } )
)
</div>
<div class="col-md-8" style="height:100%">
<div class="col-md-12" style="height:100%;">
<div id="AttrGridDiv" class="col-md-5" style="height:40%;">
<label>Attributes</label>
#await Component.InvokeAsync( "PageAttributes", new { pageId = 123 } )
</div>
</div>
</div>
and here is my component view:
#model IEnumerable<aaaaaaaa>
#(Html.Kendo().Grid<dynamic>()
.Name( "Attributes" )
.Selectable()
.Sortable()
.Scrollable()
.Events( e => e.Change( "onAttributeGridSelected" ) )
.HtmlAttributes( new { style = "width:100%;" } )
//.DataSource( ds => ds
//.Ajax()
//.Read( r => r.Action( "GetPages", "Home" ).Type( HttpVerbs.Post ) ) )
)
and here is my View Component:
[ViewComponent(Name ="PageAttributes")]
public class PageAttibutesViewComponent : ViewComponent
{
private IRulesEngineService reService;
public PageAttibutesViewComponent( IRulesEngineService _reService)
{
reService = _reService;
}
public async Task<IViewComponentResult> InvokeAsync(int pageId)
{
var pageAttribs = await reService.GetAllPageAttributesAsync( pageId );
return View( pageAttribs );
}
}
I have only shown one of the components. I have a few similar components that I need to invoke based on which listbox is selected the components will render different controls.
So, again, my questions are:
How to invoke the component from the view Conditionally?
How to pass the selected id from the chosen listbox to the invoke
method?
ViewComponents are processed by Razor (or any alternative view engine that supports the feature). What you try to achieve happens in the browser, but you need some server-side processing as well.
As I see it, you must (1) capture the ListBox change (Javascript), then (2) post it back to the controller (again Javscript) which (3) will process the request, change the model depending on what was submitted (C#), and finally (4) return the view with changed model, so that you the view can figure out how (what param value) to invoke the ViewComponent in question (C#, Razor). You may also decide to return a different view, but that is somewhat uncommon. Not so very straightforward, but this is web. Long ago, in web forms check boxes and drop downs had property PostBack (or simillar), which, when checked, did basically the same.
In effect, the post back to the controller, the changed view model, and finally the view, rendered with the changed model result in change of ViewComponent's invocation.
There are three types of ViewComponent invocations in Core 2.0. You can read about them here: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/view-components#invoking-a-view-component.
Related
This is a follow on to similar question but taking suggestions into account.
Render part of page on dropdown selection
I have a chart on my main view which I would like to update partially when a dropdown selects different values.
The page renders correctly the first time, but when I select a new value in the dropdown, then I think the .submit script is failing in the script .submit() because when I put a break on window.submitAjaxForm it is never reached.
_PnlChart.cshtml
<img src="#Url.Action("CreateTraderPnlChart3")" width="600" height="600" align="middle" vspace="50" />
My mainview Index.cshtml:
<div class="w3-half">
<div id="ExportDiv">
#{ Html.RenderPartial("_PnlChart");}
</div>
#using (Ajax.BeginForm("GetEnvironment",
new RouteValueDictionary { { "Environment", "" } }, new AjaxOptions() { UpdateTargetId = "ExportDiv" }, new { id = "ajaxForm" } ))
{
#Html.DropDownList("PeriodSelection",
new SelectList((string[])Session["Periods"]),
(string)Session["Period"],
new
{ onchange = "submitAjaxForm()" })
}
</script>
<script type="text/javascript">
$('form#ajaxForm').submit(function(event) {
eval($(this).attr('onsubmit')); return false;
});
window.submitAjaxForm = function(){
$('form#ajaxForm').submit();
}
</script>
</div>
My controller:
public ActionResult PeriodSelection(string dropdownlistReturnValue) // dont know what dropdownlistReturnValue is doing?
{
Session["Period"] = dropdownlistReturnValue;
return PartialView("~/Views/Employee/_PnlChart.cshtml");
}
This line in your code,
eval($(this).attr('onsubmit')); return false;
I am not sure what you were intending to do here. But from your question, i assume you wanted to do a form submission. But that line will not submit the form. The expression $(this).attr('onsubmit') is going to return undefined as your form does not have an onsubmit attribute defined.
But you already have the form submit code in your other method (submitAjaxForm). So if you simply remove the $('form#ajaxForm').submit handler (apparently it does not do anything useful), your code will work. When you change the dropdown, it will make an ajax form submission.
But your form action is set to GetEnvironment action method. That means your ajax form submission will be to that action method. In your question you have a different action method which returns the updated chart content. It does not makes sense!
I personally prefer to write handwritten ajax calls instead of relying on the ajax action helper methods. The below is the code i would probably use (Except the dropdownlist code. read further)
<div id="ExportDiv">
#{ Html.RenderPartial("_PnlChart");}
</div>
#Html.DropDownList("PeriodSelection",
new SelectList((string[])Session["Periods"]),
(string)Session["Period"], new
{ data_charturl = Url.Action("PeriodSelection","Home")})
Now listen to the change event of the SELECT element.
$(function(){
$("#PeriodSelection").change(function(){
var v = $(this).val();
var url=$(this).data("charturl")+'?dropdownlistReturnValue='+v;
$("#ExportDiv").load(url);
});
});
You should consider using the a view model to pass the Dropdownlist data. Why not use the DropDownListFor helper method ? It looks much clean, Mixing a lot of C# code (See all the session casting and all.) makes it kind of dirty IMHO.
I need to persist changes to a view model that contains a collection, but everytime i post back to the controller, i am losing my model bindings. I am fairly new to MVC so i may be missing something glaring here.
#{ Html.RenderAction("TabList", "TabController", new {Id = Model.Id}); }
I have a main container page that has a render action to a controller to return the first partial view.
[HttpGet]
public ViewResult TabList(Guid orderid)
{
// build the viewmodel
return View("ControlTabList", model);
}
From there iterate over the collection and different render partials based on the object type. (I have simplified the code here, as the items are polymorphic and have some type of downcasting)
#model TabListViewModel
#using (Html.BeginForm("UpdateItem", "TabController", FormMethod.Post, new {Id = "myForm"}))
{
#Html.AntiForgeryToken()
<input type="submit" value="Send" id="submitButton"/>
#for (int i = 0, c = this.Model.Count; i < c; i++)
{
var currentItem = this.Model.ElementAt(i);
#switch (currentItem.Code)
{
case "1":
Html.RenderPartial("Partials/ItemOne", currentItem);
break;
case "2":
Html.RenderPartial("Partials/ItemTwo",currentItem);
break;
default:
Html.RenderPartial("Partials/ItemThree",currentItem);
break;
}
}
}
When I post back to the controller my ViewModel will always be null.
[HttpPost]
public ActionResult UpdateItems(TabListViewModel model)
{
/* i will remove the redirect here, as the model above is always null*
}
Is there a reason why i am losing the bindings? I would like to save the entire collection, instead of individually saving each item in the collection.
There are 2 reasons why your implementation will fail.
First is the use of partials to display each item in the collection. If you inspect the html your generating you will see that the id and name attributes for each currentItem are identical. Duplicate id's are invalid html and duplicate name attributes means that you cannot bind back to a collection. Assuming currentItem has a property string Name, then the correct name would be <input name="Name[0]../>, <input name="Name[1]../> etc. Note the indexers in the name attribute, which allow you to bind to a collection.
Second, your TabListViewModel appears to be collection of a base type where you have added derived types. When you post back, the DefaultModelBinder will only initialize items of the base type since it has no way of knowing which derived type to initialize. From your last question, I assume the base type is ProvinceViewModel and the derived types are QuebecViewModel and OntarioViewModel. Since ProvinceViewModel is abstract, it cant be initialized (no constructor) so your model will always be null. While it is possible to write a custom abstract ModelBinder, as a self confessed newbie, this might be best left until you have a better knowledge of MVC and the model binding process (this article will help you get started)
The easiest way to solve this is with a view model containing collections of each type and use for loops or custom EditorTemplates. For example
View model
public class ProvinceVM
{
public List<QuebecViewModel> QuebecProvinces { get; set; }
public List<OntarioViewModel> OntarioProvinces { get; set; }
}
Then create an EditTemplate for each type
In /Views/Shared/EditorTemplates/QuebecViewModel.cshtml
#model QuebecViewModel
#Html.TextBoxFor(m => m.someProperty)
....
Then in the main view
#model ProvinceVM
#using (Html.BeginForm())
{
#Html.EditorFor(m => m.QuebecProvinces)
// Ditto for OntarioProvinces, or you can use a `for` loop as follows
for(int i = 0; i < Model.OntarioProvinces.Count; i++)
{
#Html.TextBoxFor(m => m.OntarioProvinces[i].someProperty)
....
}
}
Note that both options will generate controls such as
<input name="QuebecProvinces[0].someProperty" ..../>
<input name="QuebecProvinces[1].someProperty" ..../>
which will be correctly bound when you post back to
public ActionResult UpdateItem(ProvinceVM model) // suggest you use a more appropriate name (at least pluralize it)
There seems to be way too much logic in the view. MVC was created to make use of Soc - separation of concerns ( however the view itself it not directly an observable object from the model ) . This enables a developer to write clear and precise code for each part of the system. It however, because of its flexibility as a framework , makes the decision of this up to the developer.
The idea is for clean views, light controllers and heavy classes.
You seem to be running in circles a bit here. From your code it seems to display a view that then returns a view that renders a partialview based on a parameter that is sent to it by the controller (which is building the viewmodel for the partialview ) which is received by the render action id property.
I think there are some clear considerations that need to be implemented for the codes maintainability
First off. It seems as though you want to achieve the display of a partialview. This partialview is determined based on a parameter, the Model.Id . You do not say how this parameter is being injecting into the RenderAction but that should not matter.
#{ Html.RenderAction("TabList", "TabController", new {Id = Model.Id}); }
This code above does not need to exist. It is calling itself. And if that is not the case ( i might not fully understand why you have that there ) then that should be replaced with the call to the partial view, With the main view that the code is sitting on being injected with the ModelId parameter . In this use case that is the View TabList itself.
[HttpGet]
// Model id is passed into the controller in which ever fashion it was
// used to pass into the RenderAction method
public ViewResult TabList(int modelId = 3) // allows for a default on 3
{
// build the viewmodel
return View();
}
"Build the viewmodel" is where the main code happens, this is the stuff that determines what is going to be displayed, and so because it is not directly a display element it needs to sit in the controller. Think of it like this. When the view gets its model, everything the view needs to display should already be in the model. And If you are using polymorphism then there is no reason to be writing switch cases inside of for loops in a view . Just no. That should be sent off to its corresponding interfaces
So to build the viewmodel could be something like this
//build viewmodel
TabListViewModel model = new TabListViewModel{
PartialStuff = dbcontext.entity.FirstOrDefault(_ => _.ModelId == modelId),
}
So now you have a viewmodel with objects that has been filtered based on the modelId , and now just return that model.
return View(model);
The View then declares that model
#model TabListViewModel
And you have one partial view that receives only the objects it needs based on the model
#Render.PartialView("_ItemStuff",Model.PartialStuff)
The partial view then has its model defined as "Partialstuff" and the form objects are created by that model . This allows for a strongly typed model that will pass values back to the controller.
#model PartialStuff
#using (Html.BeginForm("UpdateItem", "TabController", FormMethod.Post, new{Id = "myForm"}))
{
#Html.AntiForgeryToken()
<input type="submit" value="Send" id="submitButton"/>
// Add values with the model directly attached
#Html.TextBoxFor(_ => _.stuffFromTheModel)
I updated the following code using the EditorTemplate to get rid of the heavy switch statements i was using to iterate over the polymorphic collection of derived types.
#for (int i = 0, c = this.Model.Count; i < c; i++)
{
var currentItem = this.Model.ElementAt(i);
#Html.EditorFor(model => currentItem)
}
I then added the following prefix in all of my EditorTemplates to prevent duplicate name and ids of the controls.
#{
ViewData.TemplateInfo.HtmlFieldPrefix = Model.ProvinceCode;
}
I created a custom ModelBinder to intercept and bind the form data to create a ViewModel i can use.
[HttpPost]
public ActionResult UpdateItem([ModelBinder(typeof(ProvinceCustomModelBinder))]ProvinceVM model)
I am using KendoUI cascading Drop-downs and it seems to be working fine for most part but it seems to have a little problem. However, I think it is a problem with my implementation NOT with the tool because on their Demo page here it seems to work fine. Also, I tried to follow the code exactly as it as to make sure that I do get the same behavior. Now, I am seeing following behavior:
I select an option in parent drop down and then click on the child drop down then it calls the conroller action correctly
It continues to do that as far as results for child dropdown are empty
Once it gets some value to bind child drop-down with, it stops making any call to the controller despite of what I choose in parent drop-down or child-dropdown.
I am not sure why it happens. Following is my code snippet:
Controller code
[HttpGet]
public ActionResult FindAlignmentsByTeamId(int teamId)
{
var teams = Client.GetAlignmentsByTeamId(teamId);
return Json(teams, JsonRequestBehavior.AllowGet);
}
.cshtml code
#* Perent Dropbox *#
<tr>
<td>EmployeeID</td>
<td><b>#Model.EmployeeId</b></td>
</tr>
<tr>
<td>Team</td>
<td>
#(Html.Kendo().DropDownList()
.Name("Team")
.DataTextField("TeamName")
.DataValueField("TeamId")
.DataSource(source => source.Read(read => read.Action("GetAllTeams", "Employee")))
)
</td>
</tr>
#* Child DropBox *#
#(Html.Kendo().DropDownList()
.Name("Alignment")
.DataTextField("AlignmentName")
.DataValueField("AlignmentId")
.DataSource(source => source.Read(read => read.Action("FindAlignmentsByTeamId", "Employee").Data("FilterAlignment"))
.ServerFiltering(true)
)
.CascadeFrom("teamId")
.AutoBind(false)
)
<script type="text/javascript">
function FilterAlignment() {
return {
teamId: $("#Team").val()
}
};
</script>
I am not sure what happens after it is bound successfully with a value for the first time so that it has a reason to believe that now it does not have to be checking on any OnChange() events anymore? Any ideas?
The id of your parent DropDownList is "Team", not "teamId". You need to update your child DropDownList to cascade from the correct id:
#(Html.Kendo().DropDownList()
.Name("Alignment")
.DataTextField("AlignmentName")
.DataValueField("AlignmentId"
.DataSource(source => source.Read(read => read.Action("FindAlignmentsByTeamId", "Employee").Data("FilterAlignment"))
.ServerFiltering(true)
)
.CascadeFrom("Team")
.AutoBind(false)
)
I have a main view with a textbox and dropdown list:
#using (Ajax.BeginForm("GetResults", "SomeController", new AjaxOptions { HttpMethod = "Post", UpdateTargetId = "PartialDiv" }))
<select name ="dropdown1" onchange="$(this.form).submit();" >
<option></option>
#foreach (var recordrow in ...)
{
<option value="#recordrow.Value" >#recordrow.Text</option>
}
</select>
<select name ="dropdown2" >
<option></option>
#foreach (var recordrow in ...)
{
<option value="#recordrow.Value" >#recordrow.Text</option>
}
</select>
#Html.TextBox("textbox1", null, new { size = 10 })
<div id="PartialDiv" class="GroupBoxForeground" >
#Html.Partial("PartialView")
</div>
The main view automatically renders a partial view once certain controls on the main view have been filled with values. Inside the Partial View, there is a submit button which calls an ActionResult.
Part of the Partial View:
#using (Ajax.BeginForm("Function1", "SomeController", new AjaxOptions { HttpMethod = "Post", UpdateTargetId = "someDiv" }))
...
<input type="submit" name="btn_submit" value="Process" style="width:80px" />
My ActionResult looks like this:
[HttpPost]
public ActionResult Function1(FormCollection formdata, MasterModel MstModel)
{
...
}
I am able to successfully bind the values in my Partial View to MasterModel. However, I am unable to get the values of the controls in the Main View while calling the ActionResult. I cannot find the key in the FormCollection and have tried the same method I used in the partial view for model binding.
Both Views are strongly-typed with the MasterModel.
Since the values in the Main View can be changed after the Partial View is rendered, I need to get the values once more when the submit button is clicked in the partial view. Is there a way to access those control values in the Main View inside ActionResult of the Partial View?
Edit:
To explain the situation more clearly as suggested in the comments, My main view is basically a search screen with a textbox and one of the dropdown lists which will be used by the partial view for processing later.
My partial view (in a separate form) shows the results of the search along with a submit button inside to process certain tasks based on the results. However, this processing also requires the 2nd dropdown list's value and the textbox's value in the main view. Those 2 controls in the main view can be changed even after the partial view is loaded. Therefore, I am hoping to find some way to get those values in the main view when the submit button is pressed.
so you mean, you have two separate forms. On posting second form, you also want to post values of the controls, that are in first form. For that purpose, you can take help of jquery, as values outside the form are not posted on submit.
You can create hidden fields in your second form (partial view form), as:
#Html.Hidden("dropdown1value", "", new { #id = "ddl1val" })
#Html.Hidden("dropdown2value", "", new { #id = "ddl2val" })
and then on change event of your dropdown1, you can do like:
//assuming 'dropdown1' is the `id` of your dropdown1
$("#dropdown1").change(function () {
$("#ddl1val").val($("#dropdown1").val());
});
same way for the second dropdown.
Hope you have got the idea.
I'm working on an ASP.NET MVC3 project, and I have a problem with RadioButtons name generated by a for loop in a PartialView.
First, here is my code, I'll explain the exact problem just after :
Model FileUploadModel
public class FileUploadModel
{
public HttpPostedFileBase File { get; set; }
public bool IsMainFile { get; set; }
}
Model MyModel
public class MyModel
{
// Some properties
public List<FileUploadModel> Files { get; set; }
}
How the PartialView _UploadFiles is called
#Html.Partial("_UploadFiles", Model) // Model is a MyModel model
PartialView _UploadFiles - HERE IS THE PROBLEM
#model MyModel
#{
var nbFiles = (Model.Files != null ? Model.Files.Count : 0);
const int NB_DEFAULT_UPLOAD_FIELDS = 3;
}
#for (int i = 0; i < NB_DEFAULT_UPLOAD_FIELDS; i++)
{
#Html.TextBoxFor(m => m.Files[nbFiles].File, new { type = "file", name = "Files", id = "Files" })
#Html.RadioButtonFor(m => m.Files[nbFiles].IsMainFile, "Main ?", new { id = "rb" + i + "_File_IsMain" })
}
So this is the last snippet the problem. I'd like to create 3 upload fields with an associated RadioButton indicating if the file is the "main file". However, with the above snippet, the displayed view is OK, but when I validate my form, my posted model only have the first picture uploaded (and the corresponding boolean IsMainFile , the others are just ignored). That means the List Files contains only the first TextBox and first RadioButton data
So I tried with #Html.RadioButtonFor(m => m.Files[i].IsMainFile, but the RadioButtons have a different name so user can check all RadioButtons, which is NOT the desired behavior. MVC doesn't authorize me to override the name of the RadioButtons, so I can't give them my own name.
How can I generate these fields is order to only ONE Radiobutton can be checked AND my MyModel.Files property contains all chosen files ?
Thank you
I think your best bet here is to use editor templates.
First, you create a view named FileUploadModel, and place it a folder named EditorTemplates which should exist under your controller's Views folder. Note that the name of the view must match the name of your view model's class.
Its contents would look something like:
#model FileUploadModel
#Html.TextBoxFor(m => m.File, new { type = "file" })
#Html.RadioButtonFor(m => m.IsMainFile, true)
Then, back in your "_UploadFiles" view, your markup for the input field and radio button would change to:
#model MyModel
using (Html.BeginForm(...
#Html.EditorFor(m => m.Files)
...
Note that this will automatically iterate through and apply that editor template for each FileUploadModel object, and automatically name them as required. This of course assumes that the Files property in your model is populated.
Your action that accepts the post should accept type MyModel as its sole parameter, and everything should bind up automatically at post.
There are ways to avoid editor templates and build and name those fields programmatically, but this is really the preferred way, and much cleaner.
More specific to your issue... (In other words, I forgot that the code above won't group radio buttons.)
For the grouping of radio buttons to work, you should set the attribute name as follows:
#Html.RadioButtonFor(m => m.IsMainFile, true, new { Name = "IsMainFile" })
Case matters here. Note that Name must be capitalized for this to work. I'm not sure why this is the case.
The problem is that once you change that attribute, this field will no longer automatically bind in the post. This is unfortunate but understandably is as designed since, if you look at the request, I believe you will see that only the selected radio button value has been posted.
So, you could change the value of the radio button to something recognizable since just that one instance of IsMainFile will show up in the request, if I'm correct.
Perhaps you can use this specific knowledge to tweak your existing code.