Partial within a partial null exception - c#

I have a MVC form which is more complex than all of my others, utilising three models.
Company -> Base_IP -> RequestedIP which goes ViewModel -> Partial1 -> Partial2
I am using BeginCollectionItem for this has each model has a property list of the the model down from it. IE - Company has a property called baseIps, the BaseIp class has a property called requestedIps, it is requestedIps that is coming back null, the count is there on page render, but is not on submit.
When submitting to the database in the post Create(), I get nulls on the 'requestedIps' property, why is this?
I've added the offending controller and partial code samples below, not the entire thing as it's massive/redundant - any questions, please let me know.
Controller - [HttpGet]Create()
public ActionResult Create()
{
var cmp = new Company
{
contacts = new List<Contact>
{
new Contact { email = "", name = "", telephone = "" }
}, pa_ipv4s = new List<Pa_Ipv4>
{
new Pa_Ipv4
{
ipType = "Pa_IPv4", registedAddress = false, existingNotes = "", numberOfAddresses = 0, returnedAddressSpace = false, additionalInformation = "",
requestedIps = new List<IpAllocation>
{
new IpAllocation { allocationType = "Requested", cidr = "", mask = "", subnet = "" }
}
}
}
};
return View(cmp);
}
Controller - [HttpPost]Create()
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Company cmp) // does not contain properties assigned/added to in view render
{
if (ModelState.IsValid)
{
db.companys.Add(cmp);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(cmp);
}
Create View
#model Company
#using (Html.BeginForm())
{
<div id="editorRowsAsn">
#foreach (var ip in Model.pa_ipv4s)
{
#Html.Partial("Pa_IPv4View", ip)
}
</div>
<br />
<div data-role="main" class="ui-content">
<div data-role="controlgroup" data-type="horizontal">
<input type="submit" class="ui-btn" value="Create" />
</div>
</div>
}
Pa_Ipv4 View
#model Pa_Ipv4
#using (Html.BeginCollectionItem("pa_ipv4s"))
{
#Html.AntiForgeryToken()
<div id="editorRowsRIpM">
#foreach (var item in Model.requestedIps)
{
#Html.Partial("RequestedIpView", item)
}
</div>
#Html.ActionLink("Add", "RequestedManager", null, new { id = "addItemRIpM", #class = "button" }
}
RequestedIpView
#model IpAllocation
<div class="editorRow">
#using (Html.BeginCollectionItem("requestedIps"))
{
<div class="ui-grid-c ui-responsive">
<div class="ui-block-a">
<span>
#Html.TextBoxFor(m => m.subnet, new { #class = "checkFiller" })
</span>
</div>
<div class="ui-block-b">
<span>
#Html.TextBoxFor(m => m.cidr, new { #class = "checkFiller" })
</span>
</div>
<div class="ui-block-c">
<span>
#Html.TextBoxFor(m => m.mask, new { #class = "checkFiller" })
<span class="dltBtn">
<img src="~/Images/DeleteRed.png" style="width: 15px; height: 15px;" />
</span>
</span>
</div>
</div>
}
</div>

You first (outer) partial will be generating correct name attributes that relate to your model (your code does not show any controls in the Pa_Ipv4.cshtml view but I assume you do have some), for example
<input name="pa_ipv4s[xxx-xxx].someProperty ...>
however the inner partial will not because #using (Html.BeginCollectionItem("requestedIps")) will generate
<input name="requestedIps[xxx-xxx].subnet ...>
<input name="requestedIps[xxx-xxx].cidr ...>
where they should be
<input name="pa_ipv4s[xxx-xxx].requestedIps[yyy-yyy].subnet ...>
<input name="pa_ipv4s[xxx-xxx].requestedIps[yyy-yyy].cidr ...>
Normally you can pass the prefix to the partial using additional view data (refer this answer for an example), but unfortunately, you do not have access to the Guid generated by the BeginCollectionItem helper so its not possible to correctly prefix the name attribute.
The articles here and here discuss creating your own helper for handling nested collections.
Other options include using nested for loops and including hidden inputs for the collection indexer which will allow you to delete items from the collection and still be able to bind to your model when you submit the form.
for (int i = 0; i < Model.pa_ipv4s.Count; i++)
{
for(int j = 0; j < Model.pa_ipv4s[i].requestedIps.Count; j++)
{
var name = String.Format("pa_ipv4s[{0}].requestedIps.Index", i);
#Html.TextBoxFor(m => m.pa_ipv4s[i].requestedIps[j].subnet)
#Html.TextBoxFor(m => m.pa_ipv4s[i].requestedIps[j].cidr)
...
<input type="hidden" name="#name" value="#j" />
}
}
However if you also need to dynamically add new items you would need to use javascript to generate the html (refer examples here and here)

If you look at your final markup you will probably have inputs with names like
input name="subnet"
input name="cidr"
input name="mask"
This is how the form collection will appear when the form gets posted. Unfortunately this will not bind to your Company model.
Your fields will need to look like this instead
input name="Company.pa_ipv4s[0].subnet"
input name="Company.pa_ipv4s[0].cidr"
input name="Company.pa_ipv4s[0].mask"
input name="Company.pa_ipv4s[1].subnet"
input name="Company.pa_ipv4s[1].cidr"
input name="Company.pa_ipv4s[1].mask"

There are multiple ways to "fix" this, and each has its own caveats.
One approach is to setup "Editor" views (typically in ~/Views/Shared/EditorTemplates/ClassName.cshtml), and then use #Html.EditorFor(x => x.SomeEnumerable). This will not work well in a scenario in which you need to be able to delete arbitrary items from the middle of a collection; although you can still handle those cases by means of an extra property like ItemIsDeleted that you set (e.g. via javascript).
Setting up a complete example here would be lengthy, but you can also reference this tutorial: http://coding-in.net/asp-net-mvc-3-how-to-use-editortemplates/
As a start, you would create a simple template like
~/Views/Share/EditorTemplates/Contact.cshtml:
#model yournamespace.Contact
<div>
#Html.LabelFor(c => c.Name)
#Html.TextBoxFor(c => c.Name)
#Html.ValidationMessageFor(c => c.Name)
</div>
<div>
#Html.LabelFor(c => c.Email)
#Html.TextBoxFor(c => c.Email)
#Html.ValidationMessageFor(c => c.Email)
</div>
... other simple non-enumerable properties of `Contact` ...
#Html.EditorFor(c => c.pa_ipv4s) #* uses ~/Views/Shared/EditorTemplates/pa_ipv4s.cshtml *#
In your view to edit/create a Company, you would invoke this as
#Html.EditorFor(company => company.Contacts)
(Just like the EditorTemplate for Company invokes the EditorFor pa_ipv4s.)
When you use EditorFor in this way, MVC will handle the indexing automatically for you. (How you handle adding a new contact/IPv4/etc. here is a little more advanced, but this should get you started.)
MVCContrib also has some helper methods you can use for this, but it's not particularly simple from what I recall, and may tie you down to a particular MVC version.

Related

How to submit multiple identical forms with one button

I'm currently building and application in ASP.NET Core MVC and I have ran into a problem which I cannot solve.
I have a form for something and that form should contain multiple identical fields which are added dynamically (1-10). I have managed to do that by creating a ViewComponent which contains those form fields and I make an Ajax call to invoke the view component into a tab if a user chooses to add another segment of those fields.
function CallViewComponent(num_tabs) {
var data = { id: num_tabs };
$.ajax({
type: 'POST',
url: '/Create/CreateActivityForm',
cache: false,
data: data
}).done(function (result) {
var container = "#activity-" + num_tabs;
$(container).html(result);
});
}
The problem arises because each of those fields in that view component shares a name with the other fields so each time I invoke another view component the radio buttons are shared between all identical fields.
Here is a snippet of the ViewComponent:
#model CreateActivityViewModel
<div class="activity-tab">
<div class="form-group">
<label asp-for="La.OrdinalNumber">Redni broj aktivnosti</label><br />
<select asp-for="La.OrdinalNumber" class="ordinals" style="width:50%">
#foreach (var on in Model.OrdinalNumbers)
{
<option value="#on.Value">#on.Text</option>
}
</select>
</div>
<div class="form-group">
<label asp-for="La.Latype">Tip aktivnosti</label><br />
<select asp-for="La.Latype" class="activity-type" style="width:50%">
#foreach (var lt in Model.LaTypes)
{
<option value="#lt">#lt.LatypeName</option>
}
</select>
</div>
<div class="form-group">
<label asp-for="La.Laname">Naziv aktivnosti</label>
<input asp-for="La.Laname" type="text" name="La.Laname" placeholder="Unesite naziv aktivnosti" class="f1-activity-name form-control" id="f1-activity-name">
</div>
Here is my controller which returns the ViewComponent:
[HttpPost]
public IActionResult CreateActivityForm(int id)
{
return ViewComponent("ActivityTab", id);
}
Here is the Invoke method from the ViewComponent:
public IViewComponentResult Invoke(int id)
{
var latypes = _laTypeRepository.GetAllLaType.ToList();
var ordinals = new List<SelectListItem>();
var laperformances = _laPerformanceRepository.GetAllLaPerformance.ToList();
var teachingAids = _teachingAidRepository.GetAllTeachingAid.ToList();
var strategyMethods = _strategyMethodRepository.GetAllStrategyMethod.ToList();
var laCollaboration = _laCollaborationRepository.GetAllLaCollaboration.ToList();
for (int i = 1; i <= 100; i++)
{
ordinals.Add(new SelectListItem($"{ i }. aktivnost", i.ToString()));
}
return View( new CreateActivityViewModel
{
FormId = id,
LaTypes = latypes,
OrdinalNumbers = ordinals,
LaPerformances = laperformances,
StrategyMethods = strategyMethods,
Lacollaborations = laCollaboration,
TeachingAids = teachingAids,
TeachingAidUser = new List<TeachingAid>(),
TeachingAidStudent = new List<TeachingAid>()
});
}
And finally this is where the ViewComponent gets invoked. It is inside another form because I need to submit the main form and all the ViewComponents at once:
<fieldset>
<h4>Aktivnosti</h4>
<!-- Activity Tabs -->
<div id='activity-tabs'>
<!-- Activity Links -->
<ol id="#activity-links">
<li><a href='#activity-1'>#1</a></li>
<li id="add-activity"><button type="button" id='add-activity'><i class="fa fa-plus"></i></button></li>
</ol>
<!-- Activity Content -->
<div id='activity-1'>
<h3>Aktivnost #1</h3>
#await Component.InvokeAsync("ActivityTab")
</div>
</div>
<!-- Navigation Buttons -->
<div class="f1-buttons">
<button type="button" class="btn btn-previous">Prethodna</button>
<button type="submit" class="btn btn-submit">Kreiraj scenarij</button>
</div>
</fieldset>
My question is how do I separate those identical forms and be able to submit them and store every single one of those forms into an array of objects which I can then store into a database.
I am open to all ideas and will change the entire code if necessary.
Thank you!
If you have an array of objects you need to render the components using a FOR loop rather than a FOR-EACH. I like to push common code into a shared view but you can code direct in the view. You will need to set your asp-for attributes in order to bind values to the model
#for (int index = 0; index < Model.Resources.Count; index++)
{
<partial name="_ResourceHidden" for="#Model.Resources[index]" />
Direct render
#for (int index = 0; index < Model.Resources.Count; index++)
{
<tr>
<td>
#Model.Resources[index].ResourceName
</td>

.NET Multiple Data Filters

I've been using the EF6 tutorial and created filters which work great! However the guide only shows how to make one filter. I've made multiple filters, though they need different strings to operate. However pagination uses searchString (one of the filter strings) to display based on filtered results. How can I operate multiple filters alongside pagination??
Filters and pagination:
if (searchString != null)
{
page = 1;
}
else
{
searchString = currentFilter;
}
ViewBag.CurrentFilter = searchString;
var clients = from s in db.Clients.Include(c => c.MJTopics).Include(c => c.UserTable)
select s;
if (!String.IsNullOrEmpty(searchString))
{
clients = clients.Where(s => s.clientN.Contains(searchString)
|| s.homePage.Contains(searchString));
}
if (!String.IsNullOrEmpty(NameString))
{
clients = clients.Where(s => s.clientEmail.Contains(NameString)
|| s.contName.Contains(NameString));
}
if (!String.IsNullOrEmpty(RIString))
{
clients = clients.Where(s => s.clientN.Contains(RIString));
}
if (!String.IsNullOrEmpty(TopicString))
{
clients = clients.Where(s => s.MJTopics.topicalTF.Contains(TopicString));
}
Pagination view:
Page #(Model.PageCount < Model.PageNumber ? 0 : Model.PageNumber) of #Model.PageCount
#Html.PagedListPager(Model, page => Url.Action("Index",
new { page, sortOrder = ViewBag.CurrentSort, currentFilter = ViewBag.CurrentFilter }))
</div>
Filters in view:
<div class="col-md-4">
#using (Html.BeginForm())
{
<p class="filtertext">Filter Clients:</p>
<p class="filterbox">#Html.TextBox("searchString")</p>
<p class="filterbox2"><input type="submit" value="Search" class="btn btn-default btn-sm selector2" /> </p>
}
</div>
<div class="col-md-4">
#using (Html.BeginForm())
{
<p class="filtertext">Find by Name: </p>
<p class="filterbox"> #Html.TextBox("NameString")</p>
<p class="filterbox2"><input type="submit" value="Search" class="btn btn-default btn-sm selector2" /></p>
}
</div>
The PagedList.Mvc project was abandoned in 2013. The site warns:
IMPORTANT: This package is no longer maintained. Please see ernado-x/X.PagedList for a drop-in replacement.
Although 5 years later, the changes may be big enough that you can't just replace one with the other.
The Nuget package for the replacement is X.PagedList.Mvc
The examples on the Github repo show that it uses IQueryable< T> directly, without filter strings, eg :
var products = MyProductDataSource.FindAllProducts(); //returns IQueryable<Product> representing an unknown number of products. a thousand maybe?
var pageNumber = page ?? 1; // if no page was specified in the querystring, default to the first page (1)
var onePageOfProducts = products.ToPagedList(pageNumber, 25); // will only contain 25 products max because of the pageSize
ViewBag.OnePageOfProducts = onePageOfProducts;
return View();
and
<!-- output a paging control that lets the user navigation to the previous page, next page, etc -->
#Html.PagedListPager( (IPagedList)ViewBag.OnePageOfProducts, page => Url.Action("Index", new { page }) )

Passing selected button value from view to controller

I would like to pass the selected button value to the controller. See my code below.
In my controller I am passing through the ProductId which I will then use to set up my product value inside my controller.
Controller:
public ActionResult PlaceOrder(int ProductId, string OrderType)
{
// Do something
}
Inside my view I have a foreach loop which will create radio like buttons and I have also got a hiddenfor(SelectedProductId)
View:
<div class="panel panel-primary">
<div class="panel-heading">Panel Name</div>
<div class="panel-body">
<div class="row">
<div class="form-group">
#Html.HiddenFor(m => m.SelectedProductId)
#if (Model.Products != null && Model.Products.Count > 0)
{
<div class="btn-group" data-toggle="buttons">
#foreach (var product in Model.Products)
{
<label class="btn btn-default productButton">
<div class="labelProduct">#Product.Name</div>
<input type="radio" name="ProductGMX" id="#("product" + #product.Id)" autocomplete="off" checked data-id="#product.Id">
</label>
}
</div>
I will want to pass the Product Id in the ActionLink which will then pass it to the controller but I am not sure how this can be achieved
Button Click:
#Html.ActionLink("Order with standard delivery", "PlaceOrder", "Standard", new { ProductId = ?, OrderType = "Standard delivery" }, new { area = "Standard" })
#Html.ActionLink("Order with Next day Delivery", "PlaceOrder", "Elevated", new { ProductId = ?, OrderType = "NextDay" }, new { area = "Elevated", })
You either need to use JavaScript to update the ActionLink's url whenever the product changes, using the data-id from the radio button.
Or
Use submit buttons instead of ActionLinks, and set the value on the radio button to the product id. You'll need to put some logic in your controller to handle the two different buttons.
Those aren't buttons. They're links, which don't participate in the form submission.
Use real buttons, i.e. <button></button> and give them a name. Then you can see which was clicked by inspecting the Request object:
<button name="_StandardDelivery">Order with standard delivery</button>
Then in your action:
if (Request["_StandardDelivery"] != null) {
// user chose standard delivery
}

ListBoxFor not unselecting between posts

I have following code in View:
<div class="row form-group">
#Html.LabelFor(x => x.GroupCriteria.Name, Translation.Name, new { #class = "col-sm-4 control-label" })
<div class="col-sm-4">
#Html.ListBoxFor(m => m.GroupCriteria.Name, Model.NameList, new { #class = "multi-select-custom" })
</div>
</div>
...
<button type="submit" name="removeCriterion" value="Name">X</button>Name: #(string.Join(",", Model.GroupCriteria.Name))</button>
...
<button type="submit">#Translation.Update</button>
And in controller:
public ActionResult GetUsers(GroupCriteriaModel groupCriteria, string removeCriterion)
{
if("Name".Equals(removeCriterion)) groupCriteria.Name.Clear();
...
return View(modelWithCriterias);
}
Now what is happening:
When I select/unselect some values by clicking in select then click "Update", everything works fine: selection in returned page is same as what was posted.
Now i click "X" to clear "Name". "removeCriterion" is equal to "Name", so Name list is cleared (in debugger I see it is empty). Debugger in View: "Model.GroupCriteria.Name" is still empty, none of "Model.NameList" is "Selected" (set true). But returned HTML contains same selection as before (same items have attribute 'selected="selected"').
Is this some kind of caching done by MVC? How to avoid this?
Most likely it is being binded from ModelState, so you should clear it:
if("Name".Equals(removeCriterion))
{
groupCriteria.Name.Clear();
ModelState.Clear();
}
If you want to perform the operation in more specific way, you can use ModelState.Remove() in order to remove only the state related to your ListBox.

How to use DropdownListFor on Multiple model view

It's been a while since I visited StackOverflow...
So, i've started using ASP.NET MVC4 with Razor, using a Model-first approach to Entity Framework. So far i've liked it , althoug I have had problems remembering certain stuff.
For example, now i'm trying to implement a simple Login form, that includes Username + Password + UserType. However, as I recall from my experiences from MVC3, you cannot pass along two models... unless you use a Tuple.
So, my objective is to create the form via #HTML helpers, but the only one i've been unable to use is the DropdownListFor, that would bring forth the list of User Types and apply them in the form.
#model Tuple<EMS_v1.User, EMS_v1.UserType>
#{
ViewBag.Title = "Log in";
}
<section id="loginForm">
<h2>Use a local account to log in.</h2>
#using (Html.BeginForm(new { ReturnUrl = ViewBag.ReturnUrl })) {
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<fieldset>
<legend>Log in Form</legend>
<ol>
<li>
#Html.Label(" User Name")
#Html.TextBoxFor(m => m.Item1.User_Name)
#Html.ValidationMessageFor(m => m.Item1.User_Name)
</li>
<li>
#Html.Label("Password")
#Html.PasswordFor(m => m.Item1.User_Pass)
#Html.ValidationMessageFor(m => m.Item1.User_Pass)
</li>
<li>
#Html.Label("Portal Access")
>>>>> #Html.DropDownListFor(model => model.Item2.Type_Id, new SelectList(Model.Item2, "Type_Id", "Description"));
</li>
</ol>
<input type="submit" value="Log in" />
</fieldset>
}
</section>
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
}
My Entity Framework Model includes UserType (int Type_Id, String Description) and User (int Id, String User_Name, String User_Pass, UserTypeType_Id), being UserTypeType_Id a Foreign Key that refers back to UserType table.
Is there any way to generate the list or IEnumerable from the UserType table? because I'm sure the code I posted doesn't work properly.
Create a collection in the action method of controller and assign it to some Viewbag property
e.g
ViewBag.UserType = new SelectList();
this collection should be IEnumerable type object.
Now use this ViewBag prorety from view.
e.g.
#Html.DropDownList ("UserType" , "Select User Type")
You really shouldn't be using the Entity Framework entities in your views. Create a viewmodel for example:
public class LoginViewModel
{
public string Username{get;set;}
public string Password{get;set;}
public int UserTypeId{get;set;}
public IEnumerable<UserTypes> UserTypes{get;set;}
}
Then in your dropdown:
#Html.DropDownListFor(m => m.UserTypeId, Model.UserTypes.Select(t => new SelectListItem { Value = t.TypeId.ToString(), Text = t.Description }));

Categories

Resources