I am trying to create a view in my application that performs basic CRUD commands in ASP.NET Core to teach myself some new skills. I am however stuck and would appreciate some assistance please.
I would like to have each "component" of the application sitting in a partial view for maintenance going forward. I initially had my Index view use a declaration of type IEnumerable (for the for each loop):
#model IEnumerable<Project.Web.Models.Sample.SampleModel>
Which worked perfect for returning the list and rendering the page but then when trying to have my Modal window partially loaded into the page and insert data using the "CreateSample" function on the controller it was not picking up the function and failed the insert (no form action found). If I then try to add:
#model Project.Web.Models.Sample.SampleModel
to the CreateModal view page it throws an error and wont even let me render the page, I presume because its being partial loaded the app is seen as having two SampleModel declarations. If I create this page completely separate and not partially loaded with the normal #model declaration it works.
I have the basic setup going so far and have included my code for each below.
Model - SampleModel
public class SampleModel
{
public int Id { get; set; }
public string SampleText { get; set; }
}
Controller - SampleController
public class SampleController : Controller
{
public const string ControllerName = "Sample";
//Open Database Connection
private _DBContext DBDatabase = new _DBContext ();
public ActionResult Index()
{
var Model = DBDatabase.Sample.Select(s => new SampleModel
{
Id = s.Id,
SampleText = s.SampleText
}).ToList();
return PartialView(Model);
}
[ActionName("_CreateModal")]
public ActionResult InsertNewRecord()
{
var Model = DBDatabase.Sample.Select(s => new SampleModel
{
Id = s.Id,
SampleText = s.SampleText
}).ToList();
return PartialView("_CreateModal", Model);
}
Views - Index, View, Create
Index - Calls Partial Views for View and Create
#using Project.Web.Controllers
#model Project.Web.Models.Sample.SampleModel
<!--html stuff here -->
#await Html.PartialAsync("_CreateModal")
<!--more html stuff here -->
#await Html.PartialAsync("_ViewData")
View - Foreach to Loop Records
#model Project.Web.Models.Sample.SampleModel
<table style="width: 100%;" id="example">
<thead>
<tr>
<th>#</th>
<th>Sample Text</th>
<th class="text-center">Status</th>
<th class="text-center">Actions</th>
</tr>
</thead>
<tbody>
#foreach (var sample in Model)
{
<tr>
<th scope="row">#sample.Id</th>
<td>#sample.SampleText</td>
<td class="text-center">
<div class="badge badge-success">Active</div>
</td>
<td class="text-center">
<div role="group" class="btn-group-sm btn-group">
<button class="btn-shadow btn btn-primary">Edit</button>
<button class="btn-shadow btn btn-primary">Delete</button>
</div>
</td>
</tr>
}
</tbody>
</table>
Create - Insert New Record
#model Project.Web.Models.Sample.SampleModel
<form method="post" asp-action="/SampleModel/CreateSample">
<div class="form-group">
<label for="CreationTime">SampleText</label>
<div>
<input type="text" class="form-control" id="SampleText" name="SampleText" placeholder="SampleText">
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Sign up</button>
</div>
</form>
As per Ammar's comment, you've just copy-pasted the Index Controller's data access. When building a form allowing the user to create a single new item, then the pattern is to typically pre-instantiate an empty model and pass it to the view:
[ActionName("_CreateModal")]
public ActionResult InsertNewRecord()
{
var model = new SampleModel(); // If Id is a GUID, then you could assign one here
return PartialView("_CreateModal", model);
}
Related
I send from controller to view a list of objects, viewmodel is the object with some properties and pagedList, that need to be presented on page. And by pressing the button, this list need to be exported as file, that is, it need to go back to the controller and be processed there.
Model:
public class ProductsList : ListViewModel<Product>
{
public ProductsList(string prefix) : base(prefix){ }
public ProductsList(PagedList<Product> products)
{
List = products;
}
public int? ProductTypeFilter {get;set; }
public string ProductTypeFilterName {get; set;}
public string FilterBy { get; set; }
}
ListViewModel just contain PagedList.
My controller
[HttpPost]
public FileResult SaveAsFile(PagedList<Product> viewmodel)
{
...
}
And my view
#model MyProject.ViewModels.ProductsList
if (Model.List.Count > 0)
{
<table id="products_table">
<colgroup>
<col class="productType"/>
</colgroup>
<thead>
<tr>
<th >
Product type
</th>
</tr>
</thead>
<tbody>
#{ var i = 0; }
#foreach (var item in Model.List)
{
<tr>
<td onclick="window.location='#Url.Action("Details", new {id = item.Id})'">
<p>
#item.Type
</p>
</td>
}
</tr>
i++;
}
</tbody>
</table>
}
<form asp-action="SaveAsFile" enctype="multipart/form-data" method="post">
#Html.HiddenFor(m => list);
<input type="submit" value="Save as File"/>
</form>
I already have tried add to controller params tags [FromForm], [FromBody] (actually all available tags).
In view tried with hidden field in form, without it just with submit; put form on partial view; other forms: ajax, Html.ActionLink("Save as File", "SaveAsFile", new {Model}).
On debug mod Model.List has 21 items (but it can has more, like 2000 items), but when I press the button, viewmodel is creating newly.
Problem: viewmodel is creating newly and i cannot get back my full viewmodel to controller
I will be grateful for any help :)
You can set your ViewModel data in a Session variable when you send the data to your View from Controller method:
In order to setup your Session, you can follow this S.O answer
Once your Session is setup, then you can put your ViewModel in it like:
HttpContext.Session.SetObjectAsJson("ProductsList", productslist);
And then retrieve it in your POST method like this:
[HttpPost]
public FileResult SaveAsFile(PagedList<Product> viewmodel)
{
//Get your viewmodel here
var list = HttpContext.Session.GetObjectFromJson<ProductsList>("ProductsList");
}
You can also serialize your ViewModel and then send it your Controller method without using form:
Create an ActionLink:
#Html.ActionLink("Submit", "SaveAsFile", "Home", new { jsonModel= Json.Encode(Model.list) }, null)
And your Controller method:
public FileResult SaveAsFile(string jsonModel)
{
var serializer= new DataContractJsonSerializer(typeof(Model.Product));
var yourmodel= (Product)serializer.ReadObject(GenerateStreamFromString(jsonModel));
}
was trying to use Html.BeginCollectionItem to work in my application and am struggling with getting the data to post. I want to add items to a list and then post the entire list. I am using ajax and jquery to add and delete items to my list and that seems to be working. But when I post the model received in my controller is always null even though when I look at fiddler the Form Data contains all my information.
Does anyone see something simple in my code that I am doing wrong?
Main View:
#model Test.Models.TestList
#using (Html.BeginForm())
{
<div class="form-group">
<div class="row">
<label for="AssemblyText" class="col-sm-1 col-sm-offset-1 control-label">Assembly:</label>
<div class="col-sm-2">
<input type="text" id="assembly" />
</div>
<label for="QuantityText" class="col-sm-1 control-label">Quantity:</label>
<div class="col-sm-2">
<input type="text" id="Qty" />
</div>
<button type="button" id="AddAssembly">Add Button</button>
</div>
</div>
<table id="Assemblies" class="table table-striped">
<thead>
<tr>
<th>Assembly</th>
<th>Quantity</th>
<th>Action</th>
</tr>
</thead>
<tbody class="text-left">
#if (Model != null)
{
foreach (var assembly in Model.mylist)
{
#Html.Partial("AssemblyRow", assembly)
}
}
</tbody>
</table>
<div class="form-group">
<input type="submit" id="submitbtn" class="btn btn-success" value="Submit" />
</div>
}
Partial View (AssemblyRow)
#model Test.Models.Test
<tr class="editorRow">
#using (Html.BeginCollectionItem("Assembly"))
{
<td>
#Html.HiddenFor(m => m.assembly)
#Html.TextBoxFor(m => m.assembly)
</td>
<td>
#Html.HiddenFor(m => m.Qty)
#Html.TextBoxFor(m => m.Qty)
</td>
<td>
<span class="dltBtn">
Delete
</span>
</td>
}
My Models are simple and look like...
public class TestList
{
public List<Test> mylist { get; set; }
}
public class Test
{
public string assembly { get; set; }
public string Qty { get; set; }
}
My controller
[HttpPost]
public ActionResult PostMain(TestList model)
{
return View();
}
I can provide whatever other code you guys think is helpful but I tried to keep it simple with what I thought were the relevant pieces.
Thanks for any help!
Edit: Pic of fiddler
Your collection property is named mylist therefore you must pass that name to the BeginCollectionItem method
#using (Html.BeginCollectionItem("mylist"))
{
....
which will generate elements with name=mylist[xxxx].assembly" (where xxxx is a Guid) that are need to correctly bind to your model.
However, you have other issues with your code. The DefaultModelBinder binds the first name/value pair matching a model property and ignores any subsequent name/value pairs with the same name. Because you have a hidden input for each property before the textbox, only the initial values you sent to the view will be bound when you submit, not the edited values. You need to remove both hidden inputs s that the partial is
#model Test.Models.Test
<tr class="editorRow">
#using (Html.BeginCollectionItem("mylist"))
{
<td>#Html.TextBoxFor(m => m.assembly)</td>
<td>#Html.TextBoxFor(m => m.Qty)</td>
<td>
<span class="dltBtn">Delete</span>
</td>
}
</tr>
Side note: It is also unclear what the html in the initial <div class="form-group"> is for. You including 2 inputs and a button, but that will not bind to your model and will not correctly add items to your collection (your add button needs to use ajax to call a server method that returns another partial view and append it to the DOM)
I don't see a submit button, so can't really tell what you are submitting, but try changing ActionResult PostMain(TestList model) to:
ActionResult PostMain(List<Test> model)
You need to use Editor Template instead of partial views. Main controller context is not available in partial views.
There are so many posts on Stackoverflow discussing this. One of them is
ASP.NET MVC 3 - Partial vs Display Template vs Editor Template
in my create view I want to give the user the possibility to create a list of objects (of the same type). Therefore I created a table in the view including each inputfield in each row. The number of rows respective "creatable" objects is a fixed number.
Lets say there is a class Book including two properties title and author and the user should be able two create 10 or less books.
How can I do that?
I don't know how to pass a list of objects (that are binded) to the controller. I tried:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(ICollection<Book> bookList)
{
if (ModelState.IsValid)
{
foreach(var item in bookList)
db.Books.Add(item);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(articlediscounts);
}
And in the view it is:
<fieldset>
<legend>Book</legend>
<table id="tableBooks" class="display" cellspacing="0" width="100%">
<thead>
<tr>
<th>Title</th>
<th>Author</th>
</tr>
</thead>
<tbody>
#for (int i = 0; i < 10 ;i++ )
{
<tr>
<td>
<div class="editor-field">
#Html.EditorFor(model => model.Title)
#Html.ValidationMessageFor(model => model.Title)
</div>
</td>
<td>
<div class="editor-field">
#Html.EditorFor(model => model.Author)
#Html.ValidationMessageFor(model => model.Author)
</div>
</td>
</tr>
}
</tbody>
</table>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
As booklist is null, it doesn't work and I don't know how to put all created objects in this list.
If you have any suggestions I would be very thankful.
Scott Hanselman has some details on passing arrays to MVC control binding: http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx
Which is essentially: ensure your controls have the correct names: using an index for lists
Change your for loop to something like:
#for (int i = 0; i < 10 ; i++)
{
<tr>
<td>
<div class="editor-field">
<input type="text" name="book[" + i + "].Title" />
</div>
</td>
<td>
<div class="editor-field">
<input type="text" name="book[" + i + "].Author" />
</div>
</td>
</tr>
}
this will then bind to your post action automatically.
[HttpPost]
public ActionResult Create(IList<Book> bookList)
You can then show/hide these as required or use js/jquery to add them dynamically
Edit: As correctly observed by Stephen Muecke, the above answer only regards the binding from the form+fields to the HttpPost, which appears to be the emphasis of the question.
The post action in the original post is not compatible with the view. There's quite a bit of missing code in the OP that may or may not be relevant, but worth observing that if your view is for a single model, then your fail code on ModelState.IsValid needs to return a single model or your view needs to be for an IList (or similar), otherwise you won't get server-side validation (but you can still get client-side validation if you manually add it to the <input>s)
The fact you use #Html.EditorFor(model => model.Title) suggests that you have declared the model in the view as
#model yourAssembly.Book
Which allows to to post back only one Book so the POST method would need to be
public ActionResult Create(Book model)
Note that you current implementation create inputs that look like
<input id="Title" name="Title" ... />
The name attributes do not have indexers (they would need to be name="[0].Title", name="[1].Title" etc.) so cannot bind to a collection, and its also invalid html because of the duplicate id attributes.
If you want to create exactly 10 books, then you need initialize a collection in the GET method and pass the collection to the view
public ActionResult Create()
{
List<Book> model = new List<Book>();
for(int i = 0; i < 10;i++)
{
model.Add(new Book());
}
return View(model);
}
and in the view
#model yourAssembly.Book
#using (Html.BeginForm())
{
for(int i = 0; i < Model.Count; i++)
{
#Html.TextBoxFor(m => m[i].Title)
#Html.ValidationMessageFor(m => m[i].Title)
.... // ditto for other properties of Book
}
<input type="submit" .. />
}
which will now bind to your collection when you POST to
public ActionResult Create(List<Book> bookList)
Note the collection should be List<Book> in case you need to return the view.
However this may force the user to create all 10 books, otherwise validation may fail (as suggested by your use of #Html.ValidationMessageFor()). A better approach is to dynamically add new Book items in the view using either the BeginCollectionItem helper method (refer example) or a client template as per this answer.
You'd need to send a JSON object that has the list of books in it. So the first thing is to create a Model class like this:
public class SavedBooks{
public List<Book> Books { get; set; }
}
Then the Book class would have to have these 2 props:
public class Book {
public string Title { get; set; }
public string Author { get; set; }
}
Next, change your controller to use this model:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(SavedBooks model)
Then create a javascript method (using jQuery) to create a JSON object that matches the structure of the controllers SavedBooks class:
var json = { Books: [ { Title: $('#title_1').val(), Author: $('#Author_1').val() } ,
{ as many items as you want }
]
};
$.ajax(
{
url: "/Controller/Create",
type: "POST",
dataType: "json",
data: json
});
I'm writing a view that displays a list of managers. The managers have checkboxes next to their name to select them to be removed from the manager list. I am having problems binding the form submission back to my view model. Here's what the page looks like:
Here's the ViewModel for the page.
public class AddListManagersViewModel
{
public List<DeleteableManagerViewModel> CurrentManagers;
}
And here's the sub-ViewModel for each of the DeleteableManagers:
public class DeleteableManagerViewModel
{
public string ExtId { get; set; }
public string DisplayName { get; set; }
public bool ToBeDeleted { get; set; }
}
This is the code for the main View:
#model MyApp.UI.ViewModels.Admin.AddListManagersViewModel
<div class="row">
<div class="span7">
#using (Html.BeginForm("RemoveManagers","Admin"))
{
#Html.AntiForgeryToken()
<fieldset>
<legend>System Managers</legend>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Remove</th>
</tr>
</thead>
<tbody>
#Html.EditorFor(model => model.CurrentManagers)
</tbody>
</table>
</fieldset>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Delete Selected</button>
</div>
}
</div>
</div>
And this is the EditorTemplate I've created for DeleteableManagerViewModel:
#model MyApp.UI.ViewModels.Admin.DeleteableManagerViewModel
<tr>
<td>#Html.DisplayFor(model => model.DisplayName)</td>
<td>
#Html.CheckBoxFor(model => model.ToBeDeleted)
#Html.HiddenFor(model => model.ExtId)
</td>
</tr>
But when I submit the form to the controller the model comes back null! this is what I want it to do:
[HttpPost]
public virtual RedirectToRouteResult RemoveManagers(AddListManagersViewModel model)
{
foreach (var man in model.CurrentManagers)
{
if (man.ToBeDeleted)
{
db.Delete(man.ExtId);
}
}
return RedirectToAction("AddListManagers");
}
I tried following along this post: CheckBoxList multiple selections: difficulty in model bind back but I must be missing something....
Thanks for your help!
Hmm. I think this is ultimately the problem; here's what you're posing:
CurrentManagers[0].ToBeDeleted=true&CurrentManagers[0].ToBeDeleted=false&CurrentManagers[0].ExtId=X00405982144
Your model is an AddListManagersViewModel that has a collection of CurrentManagers. So, you're posting an array of DeleteableManagerViewModel, which isn't getting bound to the "wrapper" model. You can try changing the model parameter to
params DeleteableManagerViewModel[] model
I don't ever use the EditorFor extensions, though, so I'm just guessing...
I have created my own custom ValidationAttribute:
public class UrlValidationAttribute : ValidationAttribute
{
public UrlValidationAttribute() {}
public override bool IsValid(object value)
{
if (value == null)
return true;
var text = value as string;
Uri uri;
return (!string.IsNullOrWhiteSpace(text) &&
Uri.TryCreate(text, UriKind.Absolute, out uri));
}
}
I am using that on one of my models and it works perfectly. However, now I am attempting to use it on a view model:
public class DeviceAttribute
{
public DeviceAttribute(int id, attributeDefinition, String url)
{
ID = id;
Url = url;
}
public int ID { get; set; }
[UrlValidation]
public String Url { get; set; }
}
The view model is used in the partial view like this:
#model List<ICMDB.Models.DeviceAttribute>
<table class="editor-table">
#foreach (var attribute in Model)
{
<tr>
#Html.HiddenFor(a => attribute.ID)
<td class="editor-label">
#Html.LabelFor(a => attribute.Url)
</td>
<td class="editor-field">
#Html.TextBoxFor(a => attribute.Url)
#Html.ValidationMessageFor(a => attribute.Url)
</td>
</tr>
}
</table>
For some unknown reason, while the constructor for UrlValidationAttribute fires, the IsValid function doesn't fire. Any ideas?
Edit: On further investigation, it seems this is happening because the DeviceAttribute view model is actually the view model for a partial. The full page is passed a different view model that contains the list of DeviceAttribute view models. So when my controller action is called, the full page view model is constructed and its values filled, but no DeviceAttribute view models are constructed, hence why no validation is run.
I would recommend you using editor templates instead of writing foreach loops. I suppose that your main view model looks something like this:
public class MyViewModel
{
public List<DeviceAttribute> Devices { get; set; }
...
}
Now in your main view:
#model MyViewModel
#using (Html.BeginForm())
{
<table class="editor-table">
#Html.EditorFor(x => x.Devices)
</table>
<input type="submit" value="OK" />
}
and in the corresponding editor template (~/Views/Shared/EditorTemplates/DeviceAttribute.cshtml):
#model DeviceAttribute
<tr>
#Html.HiddenFor(x => x.ID)
<td class="editor-label">
#Html.LabelFor(x => x.Url)
</td>
<td class="editor-field">
#Html.TextBoxFor(x => x.Url)
#Html.ValidationMessageFor(x => x.Url)
</td>
</tr>
And your POST action takes the view model back:
[HttpPost]
public ActionResult Index(MyViewModel model)
{
...
}
Now the default model binder will successfully bind all values in the view model and kick validation.
Here's a nice blog post about templates.