I have a page with server side rendering using razor, where you can add a couple of elements from different lists, fill some fields and create a request from it on submit.
Each time an item is added/taken from any list, I send a post with submit button to a specific action, e.g. "CustomerSelected". I do this, because I need to recreate additional view components for the added item. In these methods I would like to add added objects to the db context, so on submit I can just say SaveChanges() and not have to assemble everything in the same method. But in .net core db context is per request and it is advisable to keep it that way. In this case how can I store these temporary entity objects between requests so later if someone decides to submit them I can say SaveChanges() or discard them otherwise?
I would like to have something like this:
public IActionResult CustomerAdded(int customerId)
{
var customer = _context.Customers.First(c => c.IdCustomer == customerId);
var message = _context.Messages.First(m => m.IdMessage = _idMessage);
message.Customers.Add(customer);
return View();
}
public IActionResult ItemAdded(int itemId)
{
var item = _context.Items.First(c => c.IdItem == itemId);
var message = _context.Messages.First(m => m.IdMessage = _idMessage);
message.Items.Add(item);
return View();
}
public IActionResult Submit()
{
_context.SaveChanges();
return View();
}
If this is not possible then I was thinking about adding individual elements in each method and save them there and onsubmit I would build the last final element. But if someone closes their browser without submitting then I have incomplete data laying in my database. I would have to run some kind of job to delete those and it seems to be too much for such a simple task.
It's not good idea to use server resources to track changes in such scenarios. In scenarios like shopping basket, list or batch editing it's better track changes at client-side.
Your requirement to get Views generated at server-side doesn't mean you need to track changes in DbContext. Get the index view and create view from server, but track changes on client. Then to save, post all data to the server to save changes based on the tracking info that you have.
The mechanism for client-side change tracking depends to the requirement and the scenario, for example you can track changes using html inputs, you can track changes using cookie, you can track changes using javascript objects in browser memory like angular scenarios.
Here is this post I'll show an example using html inputs and model binding. To learn more about this topic, take a look at this article by Phill Haack: Model Binding To A List.
Example
In the following example I describe a list editing scenario for a list of customers. To make it simple, I suppose:
You have a list of customers which you are going to edit at client. You may want to add, edit or delete items.
When adding new item, the row template for new row should come from server.
When deleting, you mark an item as deleted by clicking on a checkbox on the row.
When adding/editing you want to show validation errors near the cells.
You want to save changes at the end, by click on Save button.
To implement above scenario Then you need to create following models, actions and views:
Trackable<T> Model
This class is a model which helps us in client side tracking and list editing:
public class Trackable<T>
{
public Trackable() { }
public Trackable(T model) { Model = model; }
public Guid Index { get; set; } = Guid.NewGuid();
public bool Deleted { get; set; }
public bool Added { get; set; }
public T Model { get; set; }
}
Customer Model
The customer model:
public class Customer
{
[Display(Name ="Id")]
public int Id { get; set; }
[StringLength(20, MinimumLength = 1)]
[Required]
[Display(Name ="First Name")]
public string FirstName { get; set; }
[StringLength(20, MinimumLength = 1)]
[Required]
[Display(Name ="Last Name")]
public string LastName { get; set; }
[EmailAddress]
[Required]
[Display(Name ="Email Name")]
public string Email { get; set; }
}
Index.cshtml View
The Index view is responsible to render List<Trackable<Customer>>. When rendering each record, we use RowTemplate view. The same view which we use when adding new item.
In this view, we have a submit button for save and a button for adding new rows which calls Create action using ajax.
Here is Index view:
#model IEnumerable<Trackable<Customer>>
<h2>Index</h2>
<form method="post" action="Index">
<p>
<button id="create">New Customer</button>
<input type="submit" value="Save All">
</p>
<table class="table" id="data">
<thead>
<tr>
<th>
Delete
</th>
<th>
#Html.DisplayNameFor(x => x.Model.FirstName)
</th>
<th>
#Html.DisplayNameFor(x => x.Model.LastName)
</th>
<th>
#Html.DisplayNameFor(x => x.Model.Email)
</th>
</tr>
</thead>
<tbody>
#foreach (var item in Model)
{
await Html.RenderPartialAsync("RowTemplate", item);
}
</tbody>
</table>
</form>
#section Scripts{
<script>
$(function () {
$('#create').click(function (e) {
e.preventDefault();
$.ajax({
url: 'Create',
method: 'Get',
success: function (data) {
$('#data tbody tr:last-child').after(data);
},
error: function (e) { alert(e); }
});
});
});
</script>
}
RowTemplate.cshtml View
This view is responsible to render a customer record. In this view, we first render the Index in a hidden, then set a prefix [index] for the fields and then render the fields, including index again, added, deleted and model id:
Here is RowTemplate View:
#model Trackable<Customer>
<tr>
<td>
#Html.HiddenFor(x => x.Index)
#{Html.ViewData.TemplateInfo.HtmlFieldPrefix = $"[{Model.Index}]";}
#Html.HiddenFor(x => x.Index)
#Html.HiddenFor(x => x.Model.Id)
#Html.HiddenFor(x => x.Added)
#Html.CheckBoxFor(x => x.Deleted)
</td>
<td>
#Html.EditorFor(x => x.Model.FirstName)
#Html.ValidationMessageFor(x => x.Model.FirstName)
</td>
<td>
#Html.EditorFor(x => x.Model.LastName)
#Html.ValidationMessageFor(x => x.Model.LastName)
</td>
<td>
#Html.EditorFor(x => x.Model.Email)
#Html.ValidationMessageFor(x => x.Model.Email)
</td>
</tr>
CustomerController
public class CustomerController : Controller
{
private static List<Customer> list;
}
It will have the following actions.
[GET] Index Action
In this action you can load data from database and shape it to a List<Trackable<Customer>> and pass to the Index View:
[HttpGet]
public IActionResult Index()
{
if (list == null)
{
list = Enumerable.Range(1, 5).Select(x => new Customer()
{
Id = x,
FirstName = $"A{x}",
LastName = $"B{x}",
Email = $"A{x}#B{x}.com"
}).ToList();
}
var model = list.Select(x => new Trackable<Customer>(x)).ToList();
return View(model);
}
[GET] Create Action
This action is responsible to returning new row template. It will be called by a button in Index View using ajax:
[HttpGet]
public IActionResult Create()
{
var model = new Trackable<Customer>(new Customer()) { Added = true };
return PartialView("RowTemplate", model);
}
[POST] Index Action
This action is responsible for receiving the tracked item from client and save them. The model which it receives is List<Trackable<Customer>>. It first strips the validation error messages for deleted rows. Then removes those which are both deleted and added. Then checks if model state is valid, tries to apply changes on data source.
Items having Deleted property as true are deleted, items having Added as true and Deleted as false are new items, and rest of items are edited. Then without needing to load all items from database, just using a for loop, call db.Entry for each item and set their states and finally save changes.
[HttpPost]
public IActionResult Index(List<Trackable<Customer>> model)
{
//Cleanup model errors for deleted rows
var deletedIndexes = model.
Where(x => x.Deleted).Select(x => $"[{x.Index}]");
var modelStateDeletedKeys = ModelState.Keys.
Where(x => deletedIndexes.Any(d => x.StartsWith(d)));
modelStateDeletedKeys.ToList().ForEach(x => ModelState.Remove(x));
//Removing rows which are added and deleted
model.RemoveAll(x => x.Deleted && x.Added);
//If model state is not valid, return view
if (!ModelState.IsValid)
return View(model);
//Deleted rows
model.Where(x => x.Deleted && !x.Added).ToList().ForEach(x =>
{
var i = list.FindIndex(c => c.Id == x.Model.Id);
if (i >= 0)
list.RemoveAt(i);
});
//Added rows
model.Where(x => !x.Deleted && x.Added).ToList().ForEach(x =>
{
list.Add(x.Model);
});
//Edited rows
model.Where(x => !x.Deleted && !x.Added).ToList().ForEach(x =>
{
var i = list.FindIndex(c => c.Id == x.Model.Id);
if (i >= 0)
list[i] = x.Model;
});
//Reditect to action index
return RedirectToAction("Index");
}
What about dynamic form(s) with javascript and using type="hidden" or visibility
and then sending everything at once
Or using TempData with redirects and reusing that data in other views(form) as input type="hidden"
Flow:
Form1 ->
Controller's Method saves data in TempData and Redirects to Form2 View / Or ViewData and return Form2 View? ->
Form2 has TempData inserted into the form under hidden inputs ->
Submit both at once
Cookie !
public class HomeController : Controller
{
public string Index()
{
HttpCookie cookie = Request.Cookies["message"];
Message message = null;
string json = "";
if (cookie == null)
{
message = new Message();
json = new System.Web.Script.Serialization.JavaScriptSerializer().Serialize(message);
cookie = new HttpCookie("message", json);
}
Response.Cookies.Add(cookie);
return json;
}
public string CustomerAdded(int id)
{
HttpCookie cookie = Request.Cookies["message"];
Message message = null;
string json = "";
if (cookie == null || string.IsNullOrEmpty(cookie.Value))
{
message = new Message();
json = new System.Web.Script.Serialization.JavaScriptSerializer().Serialize(message);
cookie = new HttpCookie("message", json);
}
else
{
json = cookie.Value;
message = new System.Web.Script.Serialization.JavaScriptSerializer().Deserialize<Message>(json);
}
if (message.Customers == null) message.Customers = new List<int>();
if (message.Items == null) message.Items = new List<int>();
if (!message.Customers.Contains(id))
{
message.Customers.Add(id);
}
json = new System.Web.Script.Serialization.JavaScriptSerializer().Serialize(message);
cookie = new HttpCookie("message", json);
Response.Cookies.Add(cookie);
return json;
}
public string ItemAdded(int id)
{
HttpCookie cookie = Request.Cookies["message"];
Message message = null;
string json = "";
if (cookie == null || string.IsNullOrEmpty(cookie.Value))
{
message = new Message();
json = new System.Web.Script.Serialization.JavaScriptSerializer().Serialize(message);
cookie = new HttpCookie("message", json);
}
else
{
json = cookie.Value;
message = new System.Web.Script.Serialization.JavaScriptSerializer().Deserialize<Message>(json);
}
if (message.Customers == null) message.Customers = new List<int>();
if (message.Items == null) message.Items = new List<int>();
if (!message.Items.Contains(id))
{
message.Items.Add(id);
}
json = new System.Web.Script.Serialization.JavaScriptSerializer().Serialize(message);
cookie = new HttpCookie("message", json);
Response.Cookies.Add(cookie);
return json;
}
public string Submit()
{
HttpCookie cookie = Request.Cookies["message"];
Message message = null;
string json = "";
if (cookie == null || string.IsNullOrEmpty(cookie.Value))
{
return "no data";
}
else
{
json = cookie.Value;
message = new System.Web.Script.Serialization.JavaScriptSerializer().Deserialize<Message>(json);
}
Response.Cookies["message"].Value = "";
Response.Cookies["message"].Expires = DateTime.Now.AddDays(-1);
return "Submited";
}
}
Example links
http://localhost:58603/Home/CustomerAdded/1
http://localhost:58603/Home/CustomerAdded/2
http://localhost:58603/Home/Submit
Related
I have links I want to count the number of times each link is clicked by and save this counting in database using asp.net mvc5
this is my View
#model IEnumerable<RssFeedBackEnd.New>
#{
ViewBag.Title = "Show";
}
<h2>Show</h2>
*#foreach (var item in Model)
{
<h3 dir="rtl" align="right">
#item.TitleNews
</h3>
<h5 dir="rtl" align="right"> #item.Description</h5>
<p dir="rtl" align="right">
#item.TitlePage
</p>
<br />
}
and this my Model
public class New
{
[Key]
public string IDurl { get; set; }
public int ClickCount { get; set; }
}
and this my Controller
public class ShowController : Controller
{
private RssFeedDB db = new RssFeedDB();
// GET: Show
public ActionResult Show()
{
var nw = db.News;
return View(nw);
}}
I am still a beginner as you can see
I wish you help me
I am still a beginner as you can see
To find a solution to my problem
I do something similar in one of my projects, adding an 'onclick' attribute with the function I want to use, sort of:
#item.TitlePage
Then in JavaScript I define that function:
function updateItemCount(idUrl) {
//call controller action with jQuery ajax
$.ajax({
url: 'urlToController',
data: { id: idUrl }
})
}
You'll need to have defined a controller action which updates your click count
Okay, so you can use AJAX to send your idUrl to your Controller method that will process your requirement. I will start with the View part first:
#model IEnumerable<RssFeedBackEnd.New>
#{
ViewBag.Title = "Show";
}
<h2>Show</h2>
*#foreach (var item in Model)
{
<h3 dir="rtl" align="right">
#item.TitleNews
</h3>
<h5 dir="rtl" align="right"> #item.Description</h5>
<p dir="rtl" align="right">
#item.TitlePage
</p>
<br />
}
<script>
function updateLinkCount(IDurl) {
//Now generate your JSON data here to be sent to the server
var json = {
idUrl: IDurl
};
//Send the JSON data via AJAX to your Controller method
$.ajax({
url: '#Url.Action("ProcessMyURLCount", "Home")',
type: 'post',
dataType: "json",
data: { "json": JSON.stringify(json)},
success: function (data) {
if (data.success) {
console.log("Successfully incremented count");
}
else {
console.log("Could not increment count");
}
},
error: function (error) {
console.log(error)
}
});
});
</script>
And your Controller method would look like:
using System.Web.Script.Serialization;
public ActionResult ProcessMyURLCount(string json)
{
try
{
var serializer = new JavaScriptSerializer();
dynamic jsondata = serializer.Deserialize(json, typeof(object));
//Get your variables here from AJAX call
int getid= Convert.ToInt32(jsondata["idUrl"]);
if(getid > 0)
{
bool result = IncrementCountForURL(getid);
if(result==true)
{
return Json(new{success = true}, JsonRequestBehavior.AllowGet);
}
else
{
return Json(new{success = false}, JsonRequestBehavior.AllowGet);
}
}
else
{
return Json(new{success = false}, JsonRequestBehavior.AllowGet);
}
}
catch (Exception ex)
{
return Json(new{success = false}, JsonRequestBehavior.AllowGet);
}
}
And your method to increment will look like:
public bool IncrementCountForURL(int idUrl)
{
bool flg=false;
try
{
//This is my DB class to manage my context. You would need to handle it according to your DB context
using (DBManager db = new DBManager())
{
string Query = "UPDATE {yourTable} SET urlcount=urlcount+1 WHERE idUrl=#idUrl";
db.Open();
db.CreateParameters(1);
db.AddParameters(0, "#idUrl", idUrl);
int i = db.ExecuteNonQuery (CommandType.Text, Query);
if (i > 0)
{
flg = true;
}
else
{
flg=false;
}
}
}
catch (Exception ex)
{
//Handle your exception
flg=false;
}
return flg;
}
I hope this would help you achieve your functionality. Cheers!
This is the solution I came up with and works well
// GET: Links/Details/5
public ActionResult Details(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
New newss = db.News.Find(id);
if (newss == null)
{
return HttpNotFound();
}
int urlid = newss.ID;
if (Session["URLHistory"] != null)
{
List<int> HistoryURLs = (List<int>)Session["URLHistory"];
if (HistoryURLs.Exists((element => element == urlid)))
{
int i = newss.ClickCount;
newss.ClickCount = i + 0;
db.SaveChanges();
}
else
{
HistoryURLs.Add(urlid);
Session["URLHistory"] = HistoryURLs;
int i = newss.ClickCount;
newss.ClickCount = i + 1;
db.SaveChanges();
}
}
else
{
List<int> HistoryURLs = new List<int>();
HistoryURLs.Add(urlid);
Session["URLHistory"] = HistoryURLs;
int i = newss.ClickCount;
newss.ClickCount = i + 1;
db.SaveChanges();
}
return View(newss);
}
What the user does on the client side, is largely out of your ability to affect. You can request that the browser tells you of clicks via JavaScript - but that is only a request. Short of programm flow (redirecting him to the Login page on every request), you do not even have a way to force the user to log in.
The closest reliable thing I can think off would be to have all links point to a page that counts the stuff, then redirects to the actuall URL (wich would be used for all further requests).
But this is the area of User Tracking (and I hope only the Subarea of Web Analytics), wich is it's own little area. I have no experience of this area. But I did find a proper StackOverflow tag, added it and hope someone with more knowledge can help you.
In my MVC 5 "Index" views, I use a paged list and have filters applied that are just drop down lists as follows (filtering list of users by role).
public ActionResult Index(int? roleId, int? page)
{
var pageNumber = page ?? 1;
var currentRoleId = roleId ?? 0;
var Roles = db.Roles.OrderBy(r => r.Name).ToList();
ViewBag.RoleId = new SelectList(Roles, "RoleId", "Name", currentRoleId);
var users = db.Users;
ViewBag.OnePageOfUsers = users.ToPagedList(pageNumber, 20);
In my view I have the following to capture the filter change,
#Html.DropDownList("RoleId", null, new { onchange = "$(this.form).submit();" })
and the following to handle the paging with selected roleid
#Html.PagedListPager((IPagedList)ViewBag.OnePageOfUsers,
page => Url.Action("Index", new
{
page,
roleId = ViewBag.RoleId.SelectedValue
}),
PagedListRenderOptions.ClassicPlusFirstAndLast)
This all works fine, but now that I want to use a multiselect list (filter by multiple roles), I have hit a brickwall. I have got to the point where I can initialise the selected roles, and display the list, but when I go to change the filter selection, I cannot work out how to get the selection back to the controller. After lots of trial and error, my approach is as follows:
Created a view model to hold the filter list and selection.
public IEnumerable<int> SelectedRoleIds { get; set; }
public MultiSelectList Roles { get; set; }
Initialise my ViewModel in my controller...
public ActionResult Index(int ? page, int [] roles)
{
:
model.SelectedRoleIds = new int[] { 2, 3 };// just for testing
model.Roles = new MultiSelectList(RoleList, "RoleId", "Name", model.SelectedRoleIds);
In my view, I now have a ListBox for multiselection.
#Html.ListBoxFor(m => m.SelectedRoleIds, Model.Roles, new { multiple = "multiple", onchange = "$(this.form).submit();" })
And in my view I try to assign the selection...
#Html.PagedListPager((IPagedList)ViewBag.OnePageOfUsers,
page => Url.Action("Index", new
{
page,
roles = Model.SelectedRoleIds
}),
PagedListRenderOptions.ClassicPlusFirstAndLast)
Is my approach correct? I have gone around in circles, sometimes getting paging to work without filtering and vice versa. I have tried a POST action but then I can't get the paging to work. Possibly there is a much better way of achieving my goal.
Overall my approach was on track. The key part that I was missing was sending the selected values from the view to the controller, which means converting an int array (say x[] = {1,2,3}) to query parameters x=1&x=2&x=3 so it can be passed back.
Here is my simplified overall solution for filtering an MVC paged list/index view using a multiselect list.
My View Model
public class UserViewModel
{
public int [] SelectedRoleIds { get; set; }
public MultiSelectList Roles { get; set; }
}
The Controller Action
public ActionResult Index(int? page, string[] SelectedRoleIds)
{
UserViewModel model = new UserViewModel();
var users = db.Users;
var pageNumber = page ?? 1;
var RoleList = db.Roles;
model.Roles = new MultiSelectList(RoleList, "RoleId", "Name", model.SelectedRoleIds);
if (SelectedRoleIds != null)
model.SelectedRoleIds = Array.ConvertAll(SelectedRoleIds, int.Parse);
// Filter the users by role selection
if (model.SelectedRoleIds != null)
{
users = users.Where(a => model.SelectedRoleIds.All(requiredId => a.Roles.Any(role => role.RoleId == requiredId)));
}
ViewBag.OnePageOfUsers = users.ToPagedList(pageNumber, pagesize);
return View(model);
}
The View
#Html.ListBoxFor(m => m.SelectedRoleIds, Model.Roles, new { multiple = "multiple", onchange = "$(this.form).submit();" })
Reset
#Html.PagedListPager((IPagedList)ViewBag.OnePageOfUsers,
page => Url.Action("Index", new { page = page }) +
(Model.SelectedRoleIds == null ? "" : "&" + string.Join("&", ((int[])Model.SelectedRoleIds).Select(x => "SelectedRoleIds=" + Url.Encode(x.ToString())))))
#model IEnumerable<Evidencija.Models.Vozilo>
#{
ViewBag.Title = "PokreniIzvjestaj";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>PokreniIzvjestaj</h2>
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
<legend>Vozilo</legend>
<p>
#Html.DropDownList("Vozila", Model.Select(p => new SelectListItem { Text = p.VoziloID.ToString(), Value = p.VoziloID.ToString() }), "Izaberi vozilo")
</p>
<input type="submit" value="Dodaj stavku" />
</fieldset>
}
I want to send id of table vozilo to controler with dropdownlist.
Controler accepts vozilo as a parameter but it is ollways zero.
How can I solve this without using viewmodel.
[HttpPost]
public ActionResult PokreniIzvjestaj(Vozilo v)
{
ReportClass rpt = new ReportClass();
rpt.FileName = Server.MapPath("~/Reports/Vozilo.rpt");
rpt.Load();
//ReportMethods.SetDBLogonForReport(rpt);
//ReportMethods.SetDBLogonForSubreports(rpt);
// rpt.VerifyDatabase();
rpt.SetParameterValue("#VoziloId",v.VoziloID);
Stream stream = null;
stream = rpt.ExportToStream(CrystalDecisions.Shared.ExportFormatType.PortableDocFormat);
return File(stream, "application/pdf", "Vozilo.pdf");
//PortableDocFormat--pdf format
//application/pdf -- vezan za pdf format, ako je drugi tip mjenja se u zavisnosti od izabranog
//naziv.pdf -- naziv dokumenta i izabrana ekstenzija
}
[HttpGet]
public ActionResult PokreniIzvjestaj()
{
var vozila = db.Voziloes.ToList();
return View(vozila);
}
There are two method from controler.
You currently binding your drop down to a property named Vozilo. A <select> post back single value (in your case the VoziloID or the selected option. Your POST method then tries to bind a complex object Vozilo to an int (assuming VoziloID is typeofint) which of course fails and the model isnull`. You could solve this changing the method to
[HttpPost]
public ActionResult PokreniIzvjestaj(int Vozilo)
The parameter Vozilo will now contain the value of the selected VoziloID.
However it not clear why you want to "solve this without using viewmodel" when using a view model is the correct approach
View model
public class VoziloVM
{
[Display(Name = "Vozilo")]
[Required(ErrorMessage = "Please select a Vozilo")]
public int? SelectedVozilo { get; set; }
public SelectList VoziloList { get; set; }
}
Controller
public ActionResult PokreniIzvjestaj()
{
var viziloList = db.Voziloes.Select(v => v.VoziloID);
VoziloVM model = new VoziloVM();
model.VoziloList = new SelectList(viziloList)
model.SelectedVozilo = // set a value here if you want a specific option selected
return View(model);
}
[HttpPost]
public ActionResult PokreniIzvjestaj(VoziloVM model)
{
// model.SelectedVozilo contains the value of the selected option
....
}
View
#model YourAssembly.VoziloVM>
....
#Html.LabelFor(m => m.SelectedVozilo)
#Html.DropDownListFor(m => m.SelectedVozilo, Model.VoziloList, "-Please select-")
#Html.ValidationMessageFor(m => m.SelectedVozilo)
....
My dropdown is pulling and displaying the correct list, however once selected, I click save and the selected option is disregarded and once again the value is empty.
//get
public ActionResult Edit(int id)
{
Prospect prospect = db.Prospects.Find(id);
if (prospect == null)
{
return HttpNotFound();
}
ViewBag.ProductID = new SelectList(db.Products, "ProductID", "Name", prospect.Product);
return View(prospect);
}
//post
[HttpPost]
public ActionResult Edit(Prospect prospect)
{
if (ModelState.IsValid)
{
db.Entry(prospect).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
ViewBag.ProductID = new SelectList(db.Products, "ProductID", "Name", prospect.Product);
return View(prospect);
}
//view
<div class="editor-label">
#Html.LabelFor(model => model.Product)
</div>
<div class="editor-field">
#Html.DropDownList("ProductId", String.Empty)
#Html.ValidationMessageFor(model => model.Product)
</div>
Any help will be greatly appreciated
only for helpers (except display) are tied to the model. change your drop down list to
#Html.DropDownListFor(x => x.ProductID, (SelectList)ViewBag.ProductID)
where ProductID is whatever value in your model you want the selected item tied to. You also set the drop down this way by setting that value before passing it to the view
Update:
I agree with Muffin Mans answer. Using ViewBag to send drop down lists to the view can be unreliable. A different way to put the answer the muffin man provided
Add an list to your model
public List<SelectListItem> Products { get; set; }
then on your controller populate that list from the database. Muffin Man provided one way to do it. We access our data differently so I populate my list with a foreach
var products = //populate the list from your database
List<SelectListItem> ls = new List<SelectListItem>();
foreach(var temp in products){
ls.Add(new SelectListItem() { Text = temp.ProductName, Value = temp.ProductID });
}
Model.Products = ls; // set the list in your model to the select list you just built
then on your view instead of casting a view bag list to a select list you can just reference the list from the model
#Html.DropDownListFor(x => x.ProductID, Model.Products)
You shouldn't be tying your view directly to your database table type. Use a view model. Additionally this type of data belongs in your view model, not the viewbag. The view bag is great for sharing things like page title between your view and the layout page.
public class ProspectViewModel
{
public IEnumerable<SelectListItem> ProspectList { get; set; }
[DisplayName("Product")] //This is for our label
public int SelectedProspectId { get; set; }
}
Get
public ActionResult Edit(int id)
{
var prospect = db.Prospects.Find(id);
if (prospect == null)
{
return HttpNotFound();
}
var model = new ProspectViewModel
{
ProductList = db.Products.Select(x=> new SelectListItem { ... })
};
return View(model);
}
Post
[HttpPost]
public ActionResult Edit(ProspectViewModel model)
{
if (ModelState.IsValid)
{
var prospect = new Prospect { /* populate with values from model */ };
db.Prospects.Attach(prospect);
db.Entry(prospect).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
//Need to repopulate drop down list
//And we don't need to set SelectedProductId because it's already been posted back
model.ProductList = db.Products.Select(x=> new SelectListItem { ... });
return View(model);
}
View
<div class="editor-label">
#Html.LabelFor(model => model.SelectedProductId)
</div>
<div class="editor-field">
#Html.DropDownListFor(x=> x.SelectedProductId, Model.ProductList)
#Html.ValidationMessageFor(x=> x.SelectedProductId)
</div>
This is outside the scope of this answer, but you shouldn't be doing data access within your controller. Microsoft's examples show this because they are meant to be "Hello world" examples, not necessarily to be taken literally.
I am passing a model to a view with a property that is a collection of, let's say, books. In my view I am using a foreach loop to create a table of the books in my collection, each with name, author, etc.
I want the user to be able to add/edit/delete books on client side. Then I want to be able to pass back to the controller the model, with the collection of books reflecting the changes made.
Is this possible?
Solved it without the use of ajax/jquery/knockout:
Basically i needed to wrap the cshtml page in a #using (Html.BeginForm(~)){} tag, and then use a for loop (NOT a foreach) to display each of the items in the List. Then i needed to create a #Html.HiddenFor for each item in the list. When i submit the form i take the model as a parameter and the items populate the list. I cannot show my actual code, so i hastily relabeled some of the key variables so i hope you guys can make sense of it, but this is essentially how i got it to work
Here is the controller
[HttpGet]
public ActionResult BookStore(int storeId)
{
//model contains a list property like the following:
//public List<Books> BooksList { get; set; }
//pass the model to the view
var model = new BookStoreModel();
return View(model);
}
This is the view
#model BookStore.Models.BookStoreModel
#using (Html.BeginForm("BookStoreSummary", "BookStore", FormMethod.Post))
{
<fieldset>
#Html.HiddenFor(model => model.Id)
#Html.HiddenFor(model => model.BookId)
#Html.HiddenFor(model => model.LastModified)
//some html stuff here
<table id="users" class="ui-widget ui-widget-content">
<thead>
<tr class="ui-widget-header BookTable">
<th>#Html.DisplayNameFor(model => model.BookList.FirstOrDefault().Title) </th>
<th>#Html.DisplayNameFor(model => model.BookList.FirstOrDefault().Author)</th>
</tr>
</thead>
<tbody>
#for (int i = 0; i < Model.BookList.Count; i++)
{
<tr>
<td>
#Html.HiddenFor(model => model.BookList[i].Author)
#Html.DisplayFor(model => model.BookList[i].Author)
</td>
<td>
#Html.HiddenFor(model => model.BookList[i].BookId)
#Html.HiddenFor(model => model.BookList[i].Title)
#Html.DisplayFor(model => model.BookList[i].Title)
</td>
</tr>
}
</tbody>
</table>
</fieldset>
}
and the post back controller:
[HttpPost]
//[AcceptVerbs("POST")]
public ActionResult BookStoreSummary(BookStoreModel model)
{
//do stuff with model, return
return View(model);
}
Yes, absolutely possible. I'm currently using KnockOut on client and it will allow you to bind to a collection of a Javascript object, render each item as a template, add to, delete from, then post the entire collection back to server for processing. You will need to handle things like state of deleted books, and hide them from the binding, but it's all doable.
Here's the KO syntax you would need in your view:
<table>
<!-- ko foreach: {data: books } -->
<tr>
<td data-bind="text: title" />
</tr>
<!-- /ko -->
</table>
This will create a table with 1 row for each item in books.. those objects require a 'title' property which will be the only value in the table.
Knockout is a great library and I've had a lot of fun learning and developing with it lately. You can get more information on their project page here: http://knockoutjs.com/
Yes it is possible
Say you have a form and you have collection named "Books". If you want to add new book programmatically , you could use jQuery and ajax. Fist you need some Helper classes.
Following classes help you create unique book item to add to collection of books in your view. As you know every field should have unique prefix, so model binder van distinguish between form elements
public static class HtmlClientSideValidationExtensions
{
public static IDisposable BeginAjaxContentValidation(this HtmlHelper html, string formId)
{
MvcForm mvcForm = null;
if (html.ViewContext.FormContext == null)
{
html.EnableClientValidation();
mvcForm = new MvcForm(html.ViewContext);
html.ViewContext.FormContext.FormId = formId;
}
return new AjaxContentValidation(html.ViewContext, mvcForm);
}
private class AjaxContentValidation : IDisposable
{
private readonly MvcForm _mvcForm;
private readonly ViewContext _viewContext;
public AjaxContentValidation(ViewContext viewContext, MvcForm mvcForm)
{
_viewContext = viewContext;
_mvcForm = mvcForm;
}
public void Dispose()
{
if (_mvcForm != null)
{
_viewContext.OutputClientValidation();
_viewContext.FormContext = null;
}
}
}
}
public static class CollectionValidation
{
private const string idsToReuseKey = "__htmlPrefixScopeExtensions_IdsToReuse_";
public static IDisposable BeginCollectionItem(this HtmlHelper html, string collectionName)
{
var idsToReuse = GetIdsToReuse(html.ViewContext.HttpContext, collectionName);
string itemIndex = idsToReuse.Count > 0 ? idsToReuse.Dequeue() : Guid.NewGuid().ToString();
// autocomplete="off" is needed to work around a very annoying Chrome behaviour whereby it reuses old values after the user clicks "Back", which causes the xyz.index and xyz[...] values to get out of sync.
html.ViewContext.Writer.WriteLine(string.Format("<input type=\"hidden\" name=\"{0}.index\" autocomplete=\"off\" value=\"{1}\" />", collectionName, html.Encode(itemIndex)));
return BeginHtmlFieldPrefixScope(html, string.Format("{0}[{1}]", collectionName, itemIndex));
}
public static IDisposable BeginHtmlFieldPrefixScope(this HtmlHelper html, string htmlFieldPrefix)
{
return new HtmlFieldPrefixScope(html.ViewData.TemplateInfo, htmlFieldPrefix);
}
private static Queue<string> GetIdsToReuse(HttpContextBase httpContext, string collectionName)
{
// We need to use the same sequence of IDs following a server-side validation failure,
// otherwise the framework won't render the validation error messages next to each item.
string key = idsToReuseKey + collectionName;
var queue = (Queue<string>)httpContext.Items[key];
if (queue == null)
{
httpContext.Items[key] = queue = new Queue<string>();
var previouslyUsedIds = httpContext.Request[collectionName + ".index"];
if (!string.IsNullOrEmpty(previouslyUsedIds))
foreach (string previouslyUsedId in previouslyUsedIds.Split(','))
queue.Enqueue(previouslyUsedId);
}
return queue;
}
private class HtmlFieldPrefixScope : IDisposable
{
private readonly TemplateInfo templateInfo;
private readonly string previousHtmlFieldPrefix;
public HtmlFieldPrefixScope(TemplateInfo templateInfo, string htmlFieldPrefix)
{
this.templateInfo = templateInfo;
previousHtmlFieldPrefix = templateInfo.HtmlFieldPrefix;
templateInfo.HtmlFieldPrefix = htmlFieldPrefix;
}
public void Dispose()
{
templateInfo.HtmlFieldPrefix = previousHtmlFieldPrefix;
}
}
}
Then we assume you have a partial view for adding books like this :
#model Models.Book
#using (Html.BeginAjaxContentValidation("form"))
{
using (Html.BeginCollectionItem("Books"))
{
<div class="fieldcontanier">
#Html.LabelFor(model => model.Title)
#Html.EditorFor(model => model.Title)
#Html.ValidationMessageFor(model => model.Title)
</div>
<div class="fieldcontanier">
#Html.LabelFor(model => model.Author)
#Html.EditorFor(model => model.Author)
#Html.ValidationMessageFor(model => model.Author)
</div>
...
}
}
and assume there is a "add new book" link in form which you have defined following event for that in jQuery :
$('a ').click(function (e) {
e.preventDefault();
$.ajax({
url: '#Url.Action("NewBook")',
type: 'GET',
success: function (context) {
$('#books').append(context);
$("form").removeData("validator");
$("form").removeData("unobtrusiveValidation");
$.validator.unobtrusive.parse("form");
}
});
});
In above code, first you request to action called NewBook, which returns a partial view I mentioned earlier, then it loaded after other books in page and then for unobtrusive validation to apply we use last tree lines.