ASP.NET mvc : HOWTO: Update database after editing multiselectlist (listbox) - asp.net-mvc

I'm really stuck at this: I have two listboxes populated from a database. I want to copy items from one list to the other. Then the changes must be saved in the database.
This is what I've got:
Custom ViewModel:
public class StudentModel
{
public IEnumerable<SelectListItem> NormalStudentsList { get; set; }
public IEnumerable<SelectListItem> StudentsNoClassList { get; set; }
public string[] NormalSelected { get; set; }
public string[] NoClassSelected { get; set; }
public string Save { get; set; }
}
Controller:
public ActionResult IndexStudents(Docent docent, int id, int klasgroepid)
{
var studentModel = new StudentModel
{
NormalStudentsList = docent.GeefStudentenNormaalList(id, klasgroepid),
StudentsNoClassList = docent.GeefStudentenNoClassList(id, klasgroepid)
};
return View(studentModel);
}
[HttpPost, Authorize]
public ActionResult IndexStudentsResult(StudentModel model, string add, string remove)
{
ModelState.Clear();
(if! string.IsNullOrEmpty(add))
//update database
SaveState(model);
return View(model);
}
But how can I update the database?? Using UpdateModel()?
or should I work with FormCollection? But I need a formCollection to work with UpdateModel()...
The Students table has a field named "ClassID", and when copying the rows from 1 list to the other, the ID has to change from the current ClassID to "0".
How can I do that? I'm really stuck at this... hope you can help.
This is my View
<%# Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<ProjectenII.Models.Domain.StudentModel>"%>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
IndexStudents
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h2>IndexStudents</h2>
<%using (Html.BeginForm()) { %>
<%=Html.ListBoxFor(model => model.NormalSelected, new MultiSelectList(Model.NormalStudentsList, "StudentNummer", "Naam", Model.NormalSelected), new { size = "6" }); %>
<input type="submit" name="add"
id="add" value=">>" /><br />
<%=Html.ListBoxFor(model => model.NoClassSelected, new MultiSelectList(Model.StudentsNoClassList, "StudentNummer", "Naam", Model.NoClassSelected)); %>
<% } %>
<%=Html.HiddenFor(model => model.Save) %>
<input type="submit" name="apply" id="apply" value="Save!" />
</asp:Content>

Your problem is related to returning a List from the view... check this post by Phil Haack:
Model Binding To A List
Here you can see I ran into a similar problem. In my case a used checkboxes to select items in a list. The solution proposed guided me in the right direction but it wasn't the one I used, I used Phil's post.
My Post
Hope this helps.

We may also achieve using Editor helper, but making all of the multiselectlist elements selected before submit will work:
$("#NormalSelected option").prop("selected", true);
This will pass multiselectlist items to controller.

Related

AllowHtml not working for ASP.Net Mvc 3 site

We're trying to use the [AllowHtml] decoration on one of our ViewModel properties so that we can avoid the YSOD:
A potentially dangerous Request.Form value was detected from the
client (RequestText="<br>").
when we try to submit html text, like: <br>. We want to then use Server.HtmlEncode within the controller action to prevent attacks, but when we decorate the property with [AllowHtml] it has no affect, and if we try to use [ValidateInput(false)] on the controller action, it has no effect either. We saw a StackOverflow Post saying that in MVC 3 RC2 that you have to add:
ModelMetadataProviders.Current = new
DataAnnotationsModelMetadataProvider(); to the global.asax
We tried that too, even though we are using the release version of MVC 3, not RC2, but that had no effect either. Does anyone know how to fix this?
Model:
namespace UI.Models.ViewModel
{
public class CustomerRequestSupport
{
/// <summary>
/// Gets or Sets the textual description entered by the Customer for
/// the support requested.
/// </summary>
[AllowHtml]
public string RequestText { get; set; }
}
}
Controller:
[HttpPost]
[TabsActionFilter]
public ActionResult RequestSupport(CustomerRequestSupport collection)
{
if (ModelState.IsValid)
{
Ticket ticket = new Ticket();
ticket.Requestor = LoggedInCustomer;
ticket.Summary = "General Support Ticket";
ticket.Notes = Server.HtmlEncode(collection.RequestText);
var errors = _ticketService.SubmitTicket(ticket);
if (errors.Any())
{
ModelState.AddModelError("collection",
String.Format("An error has occurred in your Request for Support: " +
"{0} Please try again later or call the help desk " +
"for immediate assistance.",
errors.Aggregate((acc, st) => acc + " " + st)));
}
else
{
TempData["FlashMessage"] = String.Format("Your request for support has been " +
"submitted, the Ticket Number is: {0}.", ticket.TicketNumber);
return AutoMapView<CustomerDetails>(View("Details", base.LoggedInCustomer));
}
}
//needed for tabs to show
ViewData.CustomerContactSet(base.LoggedInCustomer);
return View();
View:
<%# Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<UI.Models.ViewModel.CustomerRequestSupport>" %>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
Request Support
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="PageTitle" runat="server">
Request Support
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="MainContent" runat="server">
<% using (Html.BeginForm())
{ %>
<%= Html.ValidationSummary() %>
<h2>Enter a description of the support needed</h2>
<%: Html.TextAreaFor( m => m.RequestText, 4, 90, null) %>
<input type="submit" value="Submit" />
<% } %>
</asp:Content>
<asp:Content ID="Content4" ContentPlaceHolderID="JavaScriptContent" runat="server">
</asp:Content>
You gotta be doing something wrong. Unfortunately as you haven't shown your example we cannot know what you are doing wrong. So let me write you a full working example:
Model:
public class MyViewModel
{
[AllowHtml]
public string RequestText { get; set; }
}
Controller:
public class HomeController: Controller
{
public ActionResult Index()
{
var model = new MyViewModel
{
RequestText = "<strong>Hello World</strong>";
};
return View(model);
}
[HttpPost]
public ActionResult Index(MyViewModel model)
{
return View(model);
}
}
View:
#model MyViewModel
#using (Html.BeginForm())
{
#Html.TextAreaFor(x => x.RequestText)
<button type="submit">OK</button>
}
So you gotta be doing something different than what I showed here. What is it?
In his answer Darin is definitely onto something when he asks
So you gotta be doing something different than what I showed here.
What is it?
I am guessing you have something else affecting the ASP.NET pipeline that is accessing the FormCollection prior to your [AllowHtml] being taken into account. Off the top of my head some common ASP.NET MVC OSS libraries that touch the pipeline are ELMAH, Glimpse, WebActivator, MvcContrib, there are many more but you get the idea.
I have to believe you are using one of the tools above or something similar. Assuming you are make sure you are on the latest release of each and check their open bug reports.
Finally, a quick way to determine if its your code, your MVC instance or an OSS library would be to create a test project. Try creating a vanilla ASP.NET MVC project. Ensure that AllowHtml works. Then add in your various OSS components until it breaks. Just be sure when you are adding in OSS components that the versions match what you are using in your current project.

ASP.net MVC2 Passing data between strongly typed views

I have the following MVC2 view that is strongly typed with a viewmodel, the viewmodel contains a list of values from one db table, I need to display a single value from a second table in the view, this is my view code
<%# Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<CustomerDatabase.WebUI.Models.CustomerSitesListViewModel> " %>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
Customer Sites
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<% foreach (var customerSite in Model.CustomerSites) { %>
<% Html.RenderPartial("CustomerSiteSummary", customerSite); %>
<%} %>
</asp:Content>
This is the viewmodel, notice i am including a Customer member in the viewmodel as i want to display the customer name in addition to the list of customer sites
namespace CustomerDatabase.WebUI.Models
{
public class CustomerSitesListViewModel
{
public IList<CustomerSite> CustomerSites { get; set; }
public PagingInfo PagingInfo { get; set; }
public Customer customer { get; set; }
}
}
This is my controller code for the view
public ViewResult List([DefaultValue(1)] int page)
{
var customerSitesToShow = customerSiteRepository.CustomerSites;
var viewModel = new CustomerSitesListViewModel
{
CustomerSites = customerSitesToShow.Skip((page - 1) * PageSize).Take(PageSize).ToList(),
PagingInfo = new PagingInfo
{
CurrentPage = page,
ItemsPerPage = PageSize,
TotalItems = customerSitesToShow.Count()
}
};
return View(viewModel); //Passed to view as ViewData.Model (or simply model)
}
This is my partial view that renders the list,
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<CustomerDatabase.Domain.Entities.CustomerSite>" %>
<div class="item">
<div class="customer-list-item">
<h2><%:Model.customer.CustomerName%></h2>
<%: Model.AddressLine1 %>
<%: Model.AddressLine2%>
Although intellisense lets me access the customer object from the view with
<h2><%:Model.customer.CustomerName%></h2>
An error is thrown when i navigate to the view,
Object reference not set to an instance of an object.
Source Error:
Line 7: <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
Line 8: <% foreach (var customerSite in Model.CustomerSites) { %>
Line 9: <%:Model.customer.CustomerName%>
Line 10: <% Html.RenderPartial("CustomerSiteSummary", customerSite); %>
Line 11: <%}
I think the error is due to the view rendering a list, i tried changing the viewmodel member to
public IList<Customer> {get; set;}
but this doesn't work either.
Can anyone suggest a way i can achieve this or offer any advice on where i am going wrong this is one problem i haven't been able to resolve after hours or researching on the Internet?
It looks like one of the model properties are not initialized. If you add a breakpoint on that line and check the variables I'm pretty sure you'll find 1 that is null.

Display a view with many to many relationship

Having problem to display a generic list with books and its authors at view state.
A book can have one, two or three authors.
How shall I display a list that contains many books with right authors in view level?
// Fullmetalboy
The following tables of the database is:
Bok (eng. Book)
BokID
Titel
Styckpris
Sammanfattning
Bok_Forfattare (eng. Book_Author)
Bok_ForfattareID
BokID
ForfattareID
Forfattare (eng. Author)
ForfattareID
Fornamn
Efternamn
<%# Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<BokButik1.ViewModels.SokningPerformSearchViewModel>"
%>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent"
runat="server">
PerformSearch
<h2>Sökresultat</h2>
<table>
<% foreach (var bok in Model.Boks) { %>
<tr>
<td><%: bok.Titel %> av <%: bok.Bok_Forfattare %></td>
<td rowspan="2"><%: bok.Kategori.KategoriNamn %></td>
<td rowspan="2"><div id="Div1"><input type="submit"
value="+ Add to cart" />
<%: bok.Sammanfattning %>
..
<% } %>
</table>
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult PerformSearch(string txtSearch, Kategori kategoriNummer)
{
Search test = new Search(txtSearch, kategoriNummer);
var asdf = test.HamtaBokListaFranSokFunktion();
var results = new SokningPerformSearchViewModel
{
Boks = asdf,
Bok_Forfattares = myIBok_ForfattareRepository.HamtaAllaBok_ForfattareNummer()
};
return View(results);
}
namespace BokButik1.ViewModels
{
public class SokningPerformSearchViewModel
{
public List<Bok> Boks { get; set; }
public List<Bok_Forfattare> Bok_Forfattares { get; set; }
}
}
I would split your ViewModel into two ViewModels:
public class SokningPerformSearchViewModel
{
public IList<BookSearchResultViewModel> Boks { get; set; }
}
public class BookSearchResultViewModel
{
public IList<Bok> Boks { get; set; }
public IList<Bok_Forfattare> Bok_Forfattares { get; set; }
}
For every book returned in the search results, create a new BookSearchResultViewModel and assign the book to Boks. Then fetch the authors and attach them to Bok_Forfattares. Next, add that book into your SokningPerformSearchViewModel.Boks list. Finally, return the SokningPerformSearchViewModel to your View.

ASP.Net MVC2 Custom Templates Loading via Ajax and Model Updating

I have a view model with a collection of other objects in it.
public ParentViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public List<ChildViewModel> Child { get; set; }
}
public ChildViewModel
{
public int Id { get; set; }
public string FirstName { get; set; }
}
In one of my views I pass in a ParentViewModel as the model, and then use
<%: Html.EditorFor(x => x) %>
Which display a form for the Id and Name properties.
When the user clicks a button I call an action via Ajax to load in a partial view which takes a collection of Child:
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IEnumerable<Child>>" %>
<%: Html.EditorFor(x => x) %>
which then uses the custom template Child to display a form for each Child passed in.
The problem I'm having is that the form created by the Child custom template does not use the naming conventions used by the DefaultModelBinder.
ie the field name is (when loaded by Ajax):
[0].FirstName
instead of:
Child[0].FirstName
So the Edit action in my controller:
[HttpPost]
public virtual ActionResult Edit(int id, FormCollection formValues)
{
ParentViewModel parent = new ParentViewModel();
UpdateModel(parent);
return View(parent);
}
to recreate a ParentViewModel from the submitted form does not work.
I'm wondering what the best way to accomplish loading in Custom Templates via Ajax and then being able to use UpdateModel is.
Couple of things to start with is that you need to remember the default ModelBinder is recursive and it will try and work out what it needs to do ... so quite clever. The other thing to remember is you don't need to use the html helpers, actual html works fine as well :-)
So, first with the Model, nothing different here ..
public class ParentViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public List<ChildViewModel> Child { get; set; }
}
public class ChildViewModel
{
public int Id { get; set; }
public string FirstName { get; set; }
}
Parent partial view - this takes an instance of the ParentViewModel
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<ParentViewModel>" %>
<h2>Parent</h2>
<%: Html.TextBox("parent.Name", Model.Name) %>
<%: Html.Hidden("parent.Id", Model.Id) %>
<% foreach (ChildViewModel childViewModel in Model.Child)
{
Html.RenderPartial("Child", childViewModel);
}
%>
Child partial view - this takes a single instance of the ChildViewModel
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<ChildViewModel>" %>
<h3>Child</h3>
<%: Html.Hidden("parent.Child.index", Model.Id) %>
<%: Html.Hidden(string.Format("parent.Child[{0}].Id", Model.Id), Model.Id)%>
<%: Html.TextBox(string.Format("parent.Child[{0}].FirstName", Model.Id), Model.FirstName) %>
Something to note at this point is that the index value is what is used for working out the unique record in the list. This does not need to be incremental value.
So, how do you call this? Well in the Index action which is going to display the data it needs to be passed in. I have setup some demo data and returned it in the ViewData dictionary to the index view.
So controller action ...
public ActionResult Index()
{
ViewData["Message"] = "Welcome to ASP.NET MVC!";
ViewData["Parent"] = GetData();
return View();
}
private ParentViewModel GetData()
{
var result = new ParentViewModel
{
Id = 1,
Name = "Parent name",
Child = new List<ChildViewModel>
{
new ChildViewModel {Id = 2, FirstName = "first child"},
new ChildViewModel {Id = 3, FirstName = "second child"}
}
};
return result;
}
In the real world you would call a data service etc.
And finally the contents of the Index view:
<form action="<%: Url.Action("Edit") %>" method="post">
<% if (ViewData["Parent"] != null) { %>
<%
Html.RenderPartial("Parent", ViewData["Parent"]); %>
<% } %>
<input type="submit" />
</form>
Saving
So now we have the data displayed how do we get it back into an action? Well this is something which the default model binder will do for you on simple data types in relatively complex formations. So you can setup the basic format of the action which you want to post to as:
[HttpPost]
public ActionResult Edit(ParentViewModel parent)
{
}
This will give you the updated details with the original ids (from the hidden fields) so you can update/edit as required.
New children through Ajax
You mentioned in your question loading in custom templates via ajax, do you mean how to give the user an option of adding in another child without postback?
If so, you do something like this ...
Add action - Need an action which will return a new ChildViewModel
[HttpPost]
public ActionResult Add()
{
var result = new ChildViewModel();
result.Id = 4;
result.FirstName = "** to update **";
return View("Child", result);
}
I've given it an id for easy of demo purposes.
You then need a way of calling the code, so the only view you need to update is the main Index view. This will include the javascript to get the action result, the link to call the code and a target HTML tag for the html to be appended to. Also don't forget to add your reference to jQuery in the master page or at the top of the view.
Index view - updated!
<script type="text/javascript">
function add() {
$.ajax(
{
type: "POST",
url: "<%: Url.Action("Add", "Home") %>",
success: function(result) {
$('#newchild').after(result);
},
error: function(req, status, error) {
}
});
}
</script>
<form action="<%: Url.Action("Edit") %>" method="post">
<% if (ViewData["Parent"] != null) { %>
<%
Html.RenderPartial("Parent", ViewData["Parent"]); %>
<% } %>
<div id="newchild"></div>
<br /><br />
<input type="submit" /> add child
</form>
This will call the add action, and append the response when it returns to the newChild div above the submit button.
I hope the long post is useful.
Enjoy :-)
Hmm... i personally would recommend to use a JSON result, instead of a HTML result, that you fiddle in the page...
makes the system cleaner. and your postback working ;-)
I found another way to accomplish this which works in my particular situation.
Instead of loading in a partial via via Ajax that is strongly typed to a child collection like:
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IEnumerable<Child>>" %>
I created a strongly typed view to the parent type and then called EditorFor on the list like so:
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Parent>" %>
<%: Html.EditorFor(x => x.ChildList) %>
This then calls a Custom Display Template and the result is that all the HTML elements get named correctly and the Default Model binder can put everything back together.

ASP.NET MVC 2 client-side validation rules not being created

MVC isn't generating the client-side validation rules for my viewmodel. The HTML just contains this:
<script type="text/javascript">
//<![CDATA[
if (!window.mvcClientValidationMetadata) { window.mvcClientValidationMetadata = []; }
window.mvcClientValidationMetadata.push({"Fields":[],"FormId":"form0","ReplaceValidationSummary":false});
//]]>
</script>
Note that Fields[] is empty!
My view is strongly-typed and uses the new strongly-typed HTML helpers (TextBoxFor(), etc).
View Model / Domain Model
public class ItemFormViewModel
{
public Item Item { get; set; }
[Required] [StringLength(100)] public string Whatever { get; set; } // for demo
}
[MetadataType(typeof(ItemMetadata))]
public class Item
{
public string Name { get; set; }
public string SKU { get; set; }
public int QuantityRequired { get; set; }
// etc.
}
public class ItemMetadata
{
[Required] [StringLength(100)] public string Name { get; set; }
[Required] [StringLength(50)] public string SKU { get; set; }
[Range(0, Int32.MaxValue)] public int QuantityRequired { get; set; }
// etc.
}
(I know I'm using a domain model as my / as part of my view model, which isn't a good practice, but disregard that for now.)
View
<%# Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<ItemFormViewModel>" %>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h2>Editing item: <%= Html.Encode(Model.Item.Name) %></h2>
<% Html.EnableClientValidation(); %>
<%= Html.ValidationSummary("Could not save the item.") %>
<% using (Html.BeginForm()) { %>
<%= Html.TextBoxFor(model => model.Item.Name) %>
<%= Html.TextBoxFor(model => model.Item.SKU) %>
<%= Html.TextBoxFor(model => model.Item.QuantityRequired) %>
<%= Html.HiddenFor(model => model.Item.ItemID) %>
<%= Html.TextBox("Whatever", Model.Whatever) %>
<input type="submit" value="Save" />
<% } %>
</asp:Content>
I included the Whatever property on the view model because I suspected that MVC wasn't recursively inspecting the sub-properties of ItemFormViewModel.Item, but even that isn't being validated? I've even tried delving into the MVC framework source code but have come up empty. What could be going on?
About five seconds after I posted the question, I realized something: My view didn't have ValidationMessage placeholders anywhere. I added <%= Html.ValidationMessageFor(model => model.Item.Name) %> and lo and behold, MVC added validation rules for Item.Name to the JS block at the bottom of the page.
It turns out that MVC does not emit client-side validation rules for a field unless you actually do one of the following:
Call Html.ValidationMessage() for the property.
Call Html.Validate() for the property. (This one won't output error messages)
Render the controls using Html.EditorForModel(). (source)
Doing any of these tells MVC, "This property of my viewmodel is editable by the user, so you should be validating it." Just using the HTML helper to stick a textbox on the page -- even if you're using the new strongly-typed helpers -- isn't enough.
I have had no luck getting this to work in MVC 2 RC. According to other questions here on SO, you have to get the MicrosoftMvcJQueryValidation.js file from the MVC Futures release, hold your left foot behind your head, and whistle Dixie for half an hour. I did this and more and have not been able to make it work.
Hopefully it will be fixed in RTM.

Resources