I have 3 models - School, Classrooms & Courses.
A school can have many classrooms, and a classroom can have many courses taught in it.
public class School
{
public int SchoolId { get; set; }
public string SchoolName { get; set; }
public List<Classroom> Classrooms { get; set; }
}
public class Classroom
{
public int ClassroomId { get; set; }
public string Room { get; set; }
public List<Course> Courses { get; set; }
public virtual int SchoolId { get; set; }
}
public class Course
{
public int CourseId { get; set; }
public string CourseName { get; set; }
public virtual int ClassroomId { get; set; }
}
I want to create CRUD actions for School, where I can add 0 to many classrooms and within each classroom create 0 to many courses all in one form. Allowing the user to dynamically add classrooms and courses within those classrooms all within creating a school
The way that I'm accomplishing this right now is hard coding (where School is the #Model):
<div class="col-md-10">
<input asp-for="#Model.SchoolName" class="form-control" />
</div>
<div class="col-md-10">
<input asp-for="#Model.Classroom[0].Room" class="form-control" />
</div>
<div class="col-md-10">
<input asp-for="#Model.Classroom[0].Course[0].CourseName" class="form-control" />
</div>
<div class="col-md-10">
<input asp-for="#Model.Classroom[0].Course[1].CourseName" class="form-control" />
</div>
<div class="col-md-10">
<input asp-for="#Model.Classroom[0].Course[3].CourseName" class="form-control" />
</div>
I want the user to be able to add more classrooms and more courses or have the ability to remove them dynamically on the form.
You can use a partial view and JQuery to create dynamic rows in an html table.
I had a asset transfer form that required at least one line item. I placed a select list for the assets and a button to add the asset and it's details (the line item) to an html table via a partial view.
Partial View
#model ITFixedAssets.Models.TransferLineItem
#{//this helps with naming the controls within the main view/page
Models.TransferLineItem TransferLineItem = new Models.TransferLineItem();
}
<tr>
<td>
<span class="glyphicon glyphicon-trash" style="padding-right:5px;" data-action="removeItem" onclick="RemoveRow(this);"></span>
</td>
<td>
<label asp-for="#TransferLineItem.Id" class="form-control" id="row" style="height:34px;margin-top:5px;"></label>
</td>
<td>
<input asp-for="#TransferLineItem.Asset" class="form-control" />
</td>
<td>
<select asp-for="#TransferLineItem.Condition" class="form-control">
<option>Please Select</option>
<option value="New">New</option>
<option value="Good">Good</option>
<option value="Bad">Bad</option>
</select>
<span asp-validation-for="#TransferLineItem.Condition" class="text-danger"></span>
</td>
<td>
<input asp-for="#TransferLineItem.AssetDescription1" class="form-control" value="#ViewBag.AssetDescription1" />
</td>
<td>
<input asp-for="#TransferLineItem.AssetDescription2" class="form-control" />
</td>
<td>
<input asp-for="#TransferLineItem.InventoryNum" class="form-control" />
</td>
<td>
<input asp-for="#TransferLineItem.SerialNumber" class="form-control" />
</td>
</tr>
When the 'Add Asset' button is clicked it calls a JS function, which uses an ajax GET to run a 'code behind' function that returns the partial view, which is basically a skeleton of a row to be placed into the html table.
JS Function and 'Code Behind' function that returns the partial view.
function AddRow() {
$.ajax({
type: "GET",
url: "Create?handler=AddLineItemRow"
}).success(function (data) {
AddNewRow(data);
});
}
public PartialViewResult OnGetAddLineItemRow()
{
PartialViewResult partialViewResult = new PartialViewResult();
partialViewResult.ViewName = "_LineItem";
return partialViewResult;
}
If successful then the partial view ('data') is passed to another JS function that adds the row to the table and alters the generic id's and names of the controls to the appropriate 'array' style that is needed when the form is submitted in order to save the line items into the database.
//appends the transfer line item table with a new row (partial view)
function AddNewRow(data)
{
//******************************************************
//*************add the new to the table**********************
//append the table with the new row
$('#LineItems').append(data);
//******************************************************
//get value of asset drop down
var asset = $("#assetList").val();
//******************************************************
//*****add the row number to the id values of the controls********
//get the length of the table for new row #
//(subtract 1, not sure why unless it's adding the headers into the count)
var rowNum = $("#LineItems tr").length - 1;
//build new name to add to the controls
var nameIndex = rowNum - 1;
var newName = "[" + nameIndex + "].";
//get the row label
var lblRow = $("#row");
//create new id for the row label
var newId = "row_" + rowNum;
//apply new id
lblRow.attr("id", newId);
//get the last row in the table
var lastRow = $("#LineItems").find("tr").last();
////format id values for the input controls
//and add names for the controls
lastRow.find("input").each(function () {
// get id of this input control
var ctrl = $(this).attr("id");
//concatenate the row number to the id
var newId = ctrl + "_" + rowNum;
//assign new id to control
$(this).attr("id", newId);
//add the index to the control'
$(this).attr("name",$(this).attr("name").replace(".", newName));
});
//update the select list (for condition) id value
var selectControl = lastRow.find("select");
//get id of the select control
var scId = selectControl.attr("id");
//concatenate the row number to the id
newId = scId + "_" + rowNum;
//assign new id to control
selectControl.attr("id", newId);
//add new name to control
selectControl.attr("name", selectControl.attr("name").replace(".", newName));
//this ajax calls the function 'OnGetPopulateLineItemRow' in the code behind
//and passes the asset # and row # as parameters. the success function
//receives the return value from the OnGetPopulateItemRow function and
//passes that 'data' to the PopulateRow function below
$.ajax({
type: "GET",
url: "Create?handler=PopulateLineItemRow",
data: { _asset: asset },
//data: { _asset: asset, row: rowNum }
dataType: 'json',
success: function (data) {
PopulateRow(asset, data, rowNum);
}
});
}
After the new row has been added to the table and the names and id's are changed, another ajax 'GET' is used to call a 'code behind' function to to get the values for the controls in the new row (as can be seen in the above snippet).
Function that gets the item details of the Asset
public JsonResult OnGetPopulateLineItemRow(string _asset)
{
//stores named value pairs for the result
Dictionary<string, string> results = new Dictionary<string, string>();
//get record from the database for the specified asset (_asset)
var asset = from a in _context.Asset select a;
asset = asset.Where(a => a.Asset1 == _asset);
//get value for each field
var description1 = from d in asset select d.AssetDescription;
var description2 = from d in asset select d.AssetDescription2;
var inventoryNum = from d in asset select d.InventoryNo;
var serialNum = from s in asset select s.SerialNo;
//add the name value pairs to the collection
results.Add("description1", description1.ToList()[0].ToString());
results.Add("description2", description2.ToList()[0].ToString());
results.Add("inventoryNum", inventoryNum.ToList()[0].ToString());
results.Add("serialNum", serialNum.ToList()[0].ToString());
return new JsonResult(results);
}
Finally these results are passed back to the success function and JQuery is used to populate the values
function PopulateRow(asset, data,rowNum) {
$("#row" + "_" + rowNum).text($("#LineItems tr").length - 1);
$("#TransferLineItem_Asset" + "_" + rowNum).val(asset);
$("#TransferLineItem_AssetDescription1" + "_" + rowNum).val(data["description1"]);
$("#TransferLineItem_AssetDescription2" + "_" + rowNum).val(data["description2"]);
$("#TransferLineItem_InventoryNum" + "_" + rowNum).val(data["inventoryNum"]);
$("#TransferLineItem_SerialNumber" + "_" + rowNum).val(data["serialNum"]);
//reset drop down
$('#assetList').prop('selectedIndex', 0);
}
Just as a side note. Until recently I used ASP.net webforms. I'm still trying to understand how to accomplish what I considered to be simple things with server side coding that really, to me, seem to be more complicated with MVC and Razor.
Related
I have a view that I had to change to add and save multiple start and end dates. I have a jQuery/jQuery-UI(datetimepicker) call that will dynamically create more input fields based on what the user wants to enter (code given below). I don't really now how to pass all the dates to the controller because the viewModel is looking for just one StartDate and EndDate. All the Id's are incremented and the class name is auto generated to hasDatePicker from the Jquery-UI plugin. I'm thinking that I can someway changing the ViewModel type into List<> might help in some sort of way but really I don't know best practices and what other options are out there.
Let me know what other information is needed.
HTML
<div class="field_wrapper2">
<table>
<tr>
<td class="DisplayFieldName">
# of start and end dates to create
</td>
<td class="DisplayFieldData" style="padding: 0px 0px 0px 5px;">
<input type="text" name="numOfDates" id="numOfDates" value="0" />
<button class="numOfDatesBtn" id="numOfDatesBtn" type="button">Add Dates</button>
</td>
</tr>
</table>
</div>
JavaScript
$(function() {
function makenewDateTimePicker(i, aObj) {
var startRow = $("<tr>").appendTo(aObj);
var startLabel = $("<td>").html("<label for='start-date-" + i + "'>Start:</label>").appendTo(startRow);
var startInput = $("<td>").html("<input type='text' id='start-date-" + i + "' />").appendTo(startRow);
var endRow = $("<tr>").appendTo(aObj);
var endLabel = $("<td>").html("<label for='end-date-" + i + "'>End:</label>").appendTo(startRow);
var endInput = $("<td>").html("<input type='text' id='end-date-" + i + "' />").appendTo(startRow);
}
$("#numOfDatesBtn").click(function() {
var num = parseInt($("#numOfDates").val());
var c = $("input[id^='start-date']").length;
c++;
for (c; c <= num; c++) {
makenewDateTimePicker(c, $(".field_wrapper2"));
}
});
});
ViewModel
public class DatePeriodViewModel
{
public Nullable<DateTime> StartDate { get; set; }
public Nullable<DateTime> EndDate { get; set; }
}
Controller
[HttpPost]
public ActionResult CreateAuction(DatePeriodViewModel viewModel) {
}
Might not be the cleanest solution but you can try capturing the submit event on the browser and then appending an array of dates to the form, which you can then access on the controller.
$("form").submit(function () {
var dates = // create an array of the dates, try using $(".hasDatePicker").each(...)
$(this).append(dates); //append to the form
return true; //return true to proceed with the submitting chain
});
I have an ViewData with my production orders in it. Im doing a thing where the operator can create a new production by selecting the production order he wants to work on, but the ideal was to show already the last production order created, instead of him having to go across all the options and select the last one. Can someone help me out?
I'm working with ASP-NET C# Razor Pages.
This is my ViewData
ViewData["production_order_ID"] = new SelectList(_context.Production_Order, "production_order_ID", "production_order_ID");
This is my page
<div class="col-md-2" style="margin-top:10px">
<label asp-for="Production.production_order_ID" class="control-label">Ordem de Produção:</label>
<select id="orderList" name="ids" asp-for="Production.production_order_ID" class="form-control" asp-items="ViewBag.production_order_ID" onchange="getSensors(); myFunction(event);"></select>
</div>
Now is showing the first item on the ViewData but i need to show the last one by default.
One way to do it is to use a viewmodel:
public class OrdersViewModel
{
public List<Order> Orders { get; set; }
public int Selected { get; set; }
}
Then in your action method create, popullate and pass the viewmodel into the view:
public IActionResult Index()
{
var orders = new List<Order>
{
new Order { OrderId = 1, OrderName = "Order # 1" },
new Order { OrderId = 2, OrderName = "Order # 2" }
};
var viewModel = new OrdersViewModel() {
Orders = orders,
Selected = orders.LastOrDefault().OrderId // get last order
};
return View(viewModel);
}
And the view:
#model MyWebApplication.Models.OrdersViewModel // use your namespace
<div class="col-md-2" style="margin-top:10px">
<label class="control-label">Ordem de Produção:</label>
<select id="orderList" name="ids" asp-for="#Model.Selected" class="form-control" asp-items="#(new SelectList(Model.Orders, "OrderId", "OrderName"))" onchange="getSensors(); myFunction(event);"></select>
</div>
So basically what i just did was
ViewData["production_order_ID"] = new SelectList(_context.Production_Order.Where(c => c.User.AspNetUser.UserName.Equals(User.Identity.Name)).OrderByDescending(c => c.production_order_ID).ToList(), "production_order_ID", "production_order_ID");
and worked just fine for what i intended to. Sorry for wasting your time guys, but thank you so much for your answer.
I have a model with 34 numbered properties in it as shown below
Public Class ViewModel
{
public string RatingCategory01 { get; set; }
public string RatingCategory02 { get; set; }
public string RatingCategory03 { get; set; }
//...and so on until category #34
}
Rather than code an input for each category in Razor Pages, I would like to use a loop to iterate through all the categories and generate the appropriate control groups. I have tried the code below:
<tbody>
#for (var i = 1; i < 35; i++)
{
string n;
#if (i > 0 && i < 10)
{
n = "RatingCategory0" + i.ToString();
}
else
{
n = "RatingCateogry" + i.ToString();
}
<tr>
<td>
<label asp-for="#string.Format("RatingCategory" + n)" class="control-label"></label>
</td>
<td>
<select asp-for="#string.Format("RatingCategory" + n)" asp-items="Model.CategoryRatingSelectList">
<option value="">Select</option>
</select>
</td>
<td>
<input asp-for="#string.Format("RemedialTime" + n)" class="form-control" />
</td>
</tr>
}
</tbody>
When I build the project and navigate to the page, I get this error:
InvalidOperationException: Templates can be used only with field
access, property access, single-dimension array index, or
single-parameter custom indexer expressions.
I'm not sure if I am on the right track here. I would really like to create a loop to generate these inputs so make future maintenance and changes easier. It's probably pretty obvious from my code/question that I am pretty new to this, so any help is appreciated.
EDIT TO ADD SOLUTION:
I used the solution provided by Ed Plunkett which I have checked below. I altered it a bit and ended up creating a new class called 'Rating' because I found that in practice I needed a more complex object. Inside my view is now
public List<Rating> Ratings = { get; set; }
In the controller, I use a loop to add as many empty ratings as I need to the list depending on the number I need.
for (var i = 0; i < 34; i++)
{
vm.Ratings.Add(new Rating());
}
Though this will likely be updated to use something other than a hard-coded number as the application evolves.
Finally, I used a loop in the view to create a group of controls for every Rating in my List. In this case it is a TableRow containing different controls in different columns:
#for (var i = 0; i < Model.Ratings.Count; i++)
{
<tr>
<td>
#Html.DisplayFor(model => model.Ratings[i].Category)
</td>
<td>
<div class="form-group">
<select asp-for="Ratings[i].RatingValue" asp-items="Model.CategoryRatingSelectList">
<option value="">Select</option>
</select>
</div>
</td>
<td>
<input asp-for="Ratings[i].RemediationMinutes" class="form-control" />
</td>
</tr>
}
I've found that the data in this group of inputs can be bound as a List by simply including
List<Rating> Ratings
in the parameters on whichever method runs when the form is submitted.
This is what you want instead of those 34 properties and their implied 34 RemedialTime siblings:
public List<String> RatingCategory { get; set; } = new List<String>();
public List<String> RemedialTime { get; set; } = new List<String>();
If you have 34 of something and the names differ only by an index number, that's a collection, not 34 distinct properties with sequentially numbered names. Then you can enumerate the 34 items with a foreach loop, or index them individually as RatingCategory[0] through RatingCategory[33]. In C#, collection indexes start at zero, so the first one is 0 and the thirty-fourth one is 33. You get used to it.
You should also look up what String.Format() does. String.Format("Foo" + 1) is exactly the same as "Foo" + 1.
You could convert your model class to dictionary;
var viewModel = new ViewModel()
{
RatingCategory01 = "a",
RatingCategory02 = "b",
RatingCategory03 = "c"
};
var dictionaryModel = viewModel.GetType()
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.ToDictionary(prop => prop.Name, prop => prop.GetValue(viewModel, null));
Then you can iterate the dictionary in the view.
I have a dataset that currently produces an output as follows:
Code:
Part 1: The View -
#foreach (var dt in Model.PlaceList) {
<tr class="Gap">
<td>
<div class="col-sm-12">
<h3>#dt.PlaceName</h3><br />
<span>#dt.OpenTimings</span><br />
<span>#dt.Slot</span><br />
<span>#dt.ActivityName</span><br />
<span>#dt.Address</span><br />
</div>
</td>
</tr>
}
Part 2: Data Retrieval from DB
var gPlaceList = (from l in _appdb.GetPlaceDetails
select new GetListPlaces {
PlaceName = l.PlaceName,
OpenTimings = l.OpenTimings,
Slot = l.Slot,
Activity = l.Activity,
Address = l.Address
}).ToList();
Part 3: Data structure used to populate the entries
public partial class GetListPlaces {
public string PlaceName { get; set; }
public string OpenTimings { get; set; }
public string Slot { get; set; }
public string Activity { get; set; }
public string Address { get; set; }
}
When looking at the result of output for one Place, we get groups of data that looke like so:
Current Output:
Place Name
Open Timings
Slot1
Activity
Address
Place Name
Open Timings
Slot2
Activity
Address
Place Name
Open Timings
Slot3
Activity
Address
Place Name
Open Timings
Slot4
Activity
Address
Place Name
Open Timings
Slot5
Activity
Address
We want to merge the result to look like this, for all results where the other 4 columns match.
Expected Output:
Place Name
Open Timings
Slot1, Slot2, Slot3, Slot4, Slot5
Activity
Address
The data here is just a sample. the real output in our website has thousands of results and merging the data like this will help us in reducing the display area as well as reduce duplication of data.
Group your data and show it comma separated using string extension method 'Join'
#foreach (var dt in Model.PlaceList.GroupBy(x=> new { x.PlaceName, x.OpenTimings, x.ActivityName, x.Address }))
{
<tr class="Gap">
<td>
<div class="col-sm-12">
<h3>#dt.Key.PlaceName</h3><br />
<span>#dt.Key.OpenTimings</span><br />
<span>#string.Join(",", dt.ToList().Select(x=>x.SlotNo))</span><br />
<span>#dt.Key.ActivityName</span><br />
<span>#dt.Key.Address</span><br />
</div>
</td>
</tr>
}
Use grouping! Just group records by the values that you expect to be same, and use Select to form a new record:
Model.PlaceList
.GroupBy(x => {x.PlaceName, x.OpenTimings, x.ActivityName, x.Address})
.Select(group => new
{
PlaceName = group.Key.PlaceName,
OpenTimings = group.Key.OpenTimings,
...,
Slots = String.Join(", ", group.Select(x => x.SlotNo))
})
Now one record will contain exactly the kind information you needed, and you can loop over the results of this query to output the view.
I'm working in MVC and was trying to use jquery to perform the following, if there is an easier way i'd be happy to hear it...
I'm looking to have my form create dynamic input to produce something for example that could look like this: *i won't know how many subcat's they are going to enter nor how many main categories.
category0 <btnInsertSubCat0>
subcat0.0
subcat0.1
subcat0.2
category1 <btnInsertSubCat1>
subcat1.0
subcat1.1
<btbInsertNewCat>
I can produce a button that appends textboxes one after another as well as a button next to it but even if I use the .live attribute I can't get the subcat button to fire. Changed code around a lot and looked at different tutorial to no avail, fig. this would be semi easy to do?
jQuery(document).ready(function () {
var procID = 0;
var orgID = 0;
$('.clickme').live('click', function () {
var newItem = $("<input type='text' name='Procedure[" + procID + "]' value='Procedure[" + procID + "] />");
var newLabel = $("<br /><label id='Label[" + procID + "]' >ProcedureID: [" + procID + "]</label>");
var newDiv = $("<div class='objective'><b>Insert Objective</b>[" + procID + "." + orgID + "]</div>");
$("#procedureHolder").append(newLabel);
$("#procedureHolder").append(newItem);
$("#procedureHolder").append(newDiv);
procID++;
});
$('.objective').live('click', function () {
var newObj = $("<input type='text' id='Objective[" + (procID - 1) + "." + orgID + "]' >ObjectiveID: [" + (procID - 1) + "." + orgID + "]</label>");
$("#procedureHolder").append(newObj);
orgID++;
});
});
I edited my post, figured it out on my own how to utilize jquery to create an unlimited number of child dynamic textboxes. They all post back in the form collection as well. I figured out why the objectives weren't showing up, turns out i was declaring ID rather than Name. Thanks!
I've used the following approach for a dynamic search screen using ASP.NET MVC. Search options where managed in a database mapped to a product. This allowed the marketing team to tweak searching results and the search options in the admin section of the website. Essentially the approach was the following:
public class FormFieldCollection : List<FormField>
{
public string FormFieldType { get; set; }
}
public class FormField
{
public string Name {get;set;}
public string Type {get;set;}
public string Value {get;set;}
public bool IsChecked {get;set;}
}
public class FormFieldModel
{
public FormFieldCollection PaymentOptions { get; set; }
}
In the view generated from either custom Helper or use a foreach.
In your Controller action something like:
public ActionResult SomeActionMethod(FormCollection formCollection)
{
//search through the results, map back to class or loop through key value pairs.
}
View Code, obviously include some nice html markup to format the form.
#Model FormFieldCollection
#{
View.Title = "Form";
Layout = "~/Views/Shared/_defaultMaster.cshtml";
}
#foreach(var formField in Model){
<input type="#formField.Type" id="#formField.Name" name="#formField.Name" value=""#formField.Value"" />
}
#using (Html.BeginForm("SomeActionMethodAdd", "ControllerName", FormMethod.Post))
{
//Submit form to
Field Name: <input type="text" value="" /> * must be unique
<input type="submit" value="Submit" />
}
Syntax mightn't be perfect just going of memory for razor.