MVC Binding to a transformed property - asp.net-mvc

I have an object class Tag {string Key; string Text;} and an object Record that is bind to a storage table.
class Record { [...Id & other properties...]
public string Name { get; set; } // "FirstRecord"
public string Tags { get; set; } // "3,4,9"
}
Updating the Name does not present a problem. But I have some difficulties with the Tags... As you can see the Tags property is the CSV of int keys (say {1:".net",2:"java",3:"perl" ...}).
In the Record's Edit View I build a dictionary with all available Tags:
Dictionary<string, string> tags = ViewData["tags"] as Dictionary<string, string>;
[...]
<div class="form-group">
<label class="col-md-2 control-label">Tags</label>
<div class="col-md-10">
#foreach (var tag in tags)
{
<div class="checkbox-inline">
<label for="tag_#tag.Key">
<input type="checkbox" id="tag_#tag.Key" value="#tag.Key" />
#tag.Value
</label>
</div>
}
</div>
</div>
And finally I have the Edit Post controller, like this
// POST: Records/Edit/5
[HttpPost, ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(string id, [Bind("Id,Name")] Record record)
{
if (id != record.Id) {
return NotFound();
}
if (ModelState.IsValid) {
try {
await repository.UpdateTableEntityAsync(record);
}
catch (Exception) { [...] }
return RedirectToAction("Index");
}
return View(record);
}
So, I am confused if I should Bind Tags, like [Bind("Id,Name,Tags")], because that value should be taken from all the checked check-boxes values, then concatenated as CSV to be ready to be updated in the storage...

If you want to bind the checkbox values to a string, you could get all the values of checkbox and set the value of model manually. Code below is for your reference.
In view, add a name property for checkbox.
<input type="checkbox" name="tag_#tag.Key" id="tag_#tag.Key" value="#tag.Key" />
In action, we could get all the values of checkbox using Request.Form.
[HttpPost, ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(string id, [Bind("Id,Name")] Record record)
{
if (id != record.Id)
{
return NotFound();
}
record.Tags = "";
foreach (var key in Request.Form.AllKeys)
{
if (key.StartsWith("tag_"))
{
record.Tags += Request.Form[key] + ",";
}
}
if (record.Tags.Length > 0)
{
record.Tags = record.Tags.Remove(record.Tags.Length - 1, 1);
}
if (ModelState.IsValid)
{
try
{
await repository.UpdateTableEntityAsync(record);
}
catch (Exception)
{
}
return RedirectToAction("Index");
}
return View(record);
}

Related

ASP.NET MVC - Checkbox list not returning all comma separated values

I have the following checkbox list:
<input type="checkbox" name="Categories" value="1" />
<input type="checkbox" name="Categories" value="2" />
My model is as follows:
public class MyModel
{
public string Categories { get; set; }
}
My controller:
public ActionResult Index(MyModel model)
{
if (ModelState.IsValid)
{
// Save data to database, and redirect to Success page.
return RedirectToAction("Success");
}
}
Selecting both checkboxes saves only one value?
You can't get comma separated value directly to server, I suggest to change class as below
public class MyModel
{
public List<string> Categories { get; set; }
}
You get list of values which checkbox selected.
If you want comma separated value then on client side need when submit form create function and need to save on hidden variable.
May this help you
Thanks
The main problem is string property stores single string, you should use collection property to bind with checkboxes e.g. List<string> as #Ajay mentioned before.
Therefore, you should use this setup:
Model
public class MyModel
{
public MyModel()
{
SelectedCategories = new List<string>();
// put categories here
Categories = new List<SelectListItem>() { ... };
}
// other properties
public List<string> SelectedCategories { get; set; }
public List<SelectListItem> Categories { get; set; }
}
View
#foreach (var item in Model.Categories)
{
<input type="checkbox" name="SelectedCategories" value="#item.Value" ... />
}
Controller
[HttpPost]
public ActionResult Index(MyModel model)
{
if (ModelState.IsValid)
{
// create comma-separated values
var selectedCategories = string.Join(",", model.SelectedCategories);
// Save data to database, and redirect to Success page.
return RedirectToAction("Success");
}
}
If you want to use CheckBoxFor helper, use SelectListItem which has Selected property with bool type, because CheckBoxFor binds for bool property:
Model
public class MyModel
{
public MyModel()
{
// put categories here
Categories = new List<SelectListItem>() { ... };
}
// other properties
public List<SelectListItem> Categories { get; set; }
}
View
#for (var i = 0; i < Model.Categories.Count; i++)
{
#Html.CheckBoxFor(model => model.Categories[i].Selected)
#Html.HiddenFor(model => model.Categories[i].Text)
#Html.HiddenFor(model => model.Categories[i].Value)
}
Controller
[HttpPost]
public ActionResult Index(MyModel model)
{
if (ModelState.IsValid)
{
string selectedCategories = string.Join(",",
model.Categories.Where(x => x.Selected == true)
.Select(x => x.Text).ToList());
// Save data to database, and redirect to Success page.
return RedirectToAction("Success");
}
}
Note:
There is a custom helper named CheckBoxListFor which should be considered to create checkbox list from List<T> property.
An example of the checkbox list implementation can be seen here.
Related problems:
Get comma-separated string from CheckboxList HTML Helper
Get Multiple Selected checkboxes in MVC

Null object from view in ASP.NET MVC Form

I'm building a simple inventory application where the user create an order by selecting the items he wants to be delivered, fill a form with the name of the recipient and then the order get processed.
First of all I have an OrdineScarico class that stores a collection of InventoryItems to be processed (like a cart), a DeliverDetails class that stores the recipient name
public class OrdineScarico
{
private List<SingoloOrdine> ordineCollection = new List<SingoloOrdine>();
// collection methods
}
public class SingoloOrdine
{
public InventoryItem InventoryItem { get; set; }
public int Qty { get; set; }
}
public class DeliverDetails
{
[Required(ErrorMessage = "Inserire il nome del ricevente")]
public string Nome { get; set; }
}
and then a ConfermaScaricoViewModel class -in a different namespace- for wrapping them up
public class ConfermaScaricoViewModel
{
public OrdineScarico OrdineScarico { get; set; }
public DeliverDetails DeliverDetails { get; set; }
}
I have these action methods in the ScaricoController
public ViewResult Conferma()
{
return View(
new ConfermaScaricoViewModel
{
OrdineScarico = GetScarico(),
DeliverDetails = new DeliverDetails()
});
}
[HttpPost]
public ViewResult Conferma(ConfermaScaricoViewModel viewModel)
{
if (ModelState.IsValid)
{
repositoryProcessor.ScaricaItem(viewModel.OrdineScarico, viewModel.DeliverDetails);
viewModel.OrdineScarico.Clear();
return View("Confermato");
}
else
{
return View(
new ConfermaScaricoViewModel
{
OrdineScarico = GetScarico(),
DeliverDetails = new DeliverDetails()
});
}
}
where GetScarico() reads the OrdineScarico instance from the active session
private OrdineScarico GetScarico()
{
OrdineScarico scarico = (OrdineScarico)Session["Scarico"];
if (scarico == null)
{
scarico = new OrdineScarico();
Session["Scarico"] = scarico;
}
return scarico;
}
This is the view code:
#model GestioneMagazzino.WebUI.Models.ConfermaScaricoViewModel
#{
ViewBag.Title = "Conferma";
}
<h2>Conferma scarico</h2>
#using (Html.BeginForm())
{
#Html.ValidationSummary()
<div class="form-group col-md-12">
<div class="row">
<label class="text-left">Ricevente:</label>
</div>
<div class="row">
#Html.TextBoxFor(model => model.DeliverDetails.Nome, new { #class="col-md-7" })
</div>
</div>
<div>
<input class="btn btn-primary" type="submit" value="Conferma" />
</div>
}
The problem is that when the POST action method is called, I get a null value for the OrdineScarico argument, and the ModelState is always false. I also tried adding an hidden field
#Html.HiddenFor(model => model.OrdineScarico)
but the OrdineScarico argument is always null when the POST method is called, while it's not when the controller renders the view.
Thanks,
Davide.
you must use #Html.HiddenFor(model => model.OrdineScarico)
also may be DeliverDetails has other field that you must set value for it.
you can use break point on line :
if (ModelState.IsValid)
in method post of Conferma.
run code and when code arrived to break point,
move cursor to the ModelState and wait to see data in it.
on values check all entities and find which property has error and you can see the message of error.
edit: cause you have not send any data for OrdineScarico. so it will be null in post action.
the OrdineScarico has a list member so
you should add this.
<input hidden name="OrdineScarico.ordineCollection[]" value="#value">

How to shorten the url when using pagination and filtering with multiple checkboxes

I am using PagedList for server side paging, and also have a textbox in the view for filtering the data, along with checkboxes to determine which fields in my model to filter based on the search text.
My current code is
View model
public class SearchPagingViewModels
{
public IPagedList<AllResolution> Resolutions { get; set; }
public string Keyword { get; set; } // serach text
public bool IsResYearChecked { get; set; } // used to filter the ResolutionYear field
public bool IsResNumChecked { get; set; } // used to filter the ResolutionNumber field
public bool IsResTextChecked { get; set; } // used to filter the ResolutionText field
}
Controller
public ViewResult Index(int? page string keyword, bool? isResYearChecked, bool? isResNumChecked, bool? isResTextChecked)
{
int pageSize = 25;
int pageNumber = (page ?? 1);
bool searchYear = isResYearChecked.GetValueOrDefault();
....
IQueryable<> resolutions = db.AllResolutions;
if (searchKeyword != null)
{
if (searchYear)
{
resolutions = resolutions.Where(x => x.ResolutionYear.Contains(searchKeyword));
}
....
}
resolutions = resolutions.OrderBy(c => c.ResolutionYear).ThenBy(c => c.ResolutionNumber);
SearchPagingViewModels model = new SearchPagingViewModels
{
Keyword = keyword,
IsResYearChecked = searchYear,
....
Resolutions = resolutions.ToPagedList(pageNumber, pageSize)
};
return View(model);
}
View
#model SearchPagingViewModels
....
#using (Html.BeginForm("Index", "Resolutions", FormMethod.Get))
{
#Html.LabelFor(m => m.Keyword)
#Html.TextBoxFor(m => m.Keyword)
#Html.LabelFor(m => m.IsResYearChecked)
#Html.CheckBoxFor(m => m.IsResYearChecked)
// .. ditto for IsResNumChecked etc
<input type="submit" value="search" />
}
<table>
<thead>
....
</thead>
<tbody>
#foreach (var task in Model.Resolutions)
{
// .... build table rows
}
</tbody>
</table>
#Html.PagedListPager(Model.Resolutions, page => Url.Action("Index", new { page, Keyword = Model.Keyword, IsResYearChecked = Model.IsResYearChecked, IsResNumChecked = IsResNumChecked IsResTextChecked = Model.IsResTextChecked }))
While this works, the issue is that the for generates a long and ugly query string, for example
.../Index?Keyword=someText&IsResYearChecked=true&IsResYearChecked=false&IsResNumChecked=false&IsResTextChecked=true&IsResTextChecked=false
And now I want to add additional bool properties for filtering the records making it even worse and potentially exceeding the query string limit.
Is there a way to shorten the URL? Would this be related to routing? Would a new ViewModel be in order to accomplish this?
Your could replace all your bool properties with an enum marked with the [Flags] attribute where each value in the enum represents a property in your model to search.
[Flags]
public enum FilterProperties
{
None = 0,
ResolutionYear = 1,
ResolutionNumber = 2,
ResolutionText = 4,
.... // more properties
}
and the view model will be
public class SearchPagingViewModels
{
public string Keyword { get; set; }
public FilterProperties Filter { get; set; }
public IPagedList<AllResolution> Resolutions { get; set; }
}
The controller method then becomes
public ViewResult Index(int? page string keyword, FilterProperties filter = FilterProperties.None)
{
IQueryable<AllResolution> resolutions = db.AllResolutions;
if (searchKeyword != null)
{
if (filter.HasFlag(FilterProperties.ResolutionYear)
{
resolutions = resolutions.Where(x => x.ResolutionYear.Contains(feyword));
}
// .... more if blocks for other enum values
}
resolutions = resolutions.OrderBy(c => c.ResolutionYear).ThenBy(c => c.ResolutionNumber);
SearchPagingViewModels model = new SearchPagingViewModels
{
Keyword = keyword,
Filter = filter,
Resolutions = resolutions.ToPagedList(pageNumber, pageSize)
};
return View(model);
}
You view will then be
#using (Html.BeginForm("Index", "Resolutions", FormMethod.Get))
{
#Html.LabelFor(m => m.Keyword)
#Html.TextBoxFor(m => m.Keyword)
#Html.ValidationMessageFor(m => m.Keyword)
#Html.HiddenFor(m => m.Filter)
foreach (Enum item in Enum.GetValues(typeof(Tables.Controllers.FilterProperties)))
{
if (item.Equals(Tables.Controllers.FilterProperties.None))
{
continue;
}
<div>
<label>
<input type="checkbox" value="#((int)(object)item)" checked=#Model.Filter.HasFlag(item) />
<span>#item</span>
</label>
</div>
}
<span id="filtererror" class="field-validation-error" hidden >Please select at least one property to search</span>
<input type="submit" value="Search" />
}
<table>
....
</table>
#Html.PagedListPager(Model.Resolutions, page => Url.Action("Index", new { page, Keyword = Model.Keyword, Filter = (int)Model.Filter }))
And then use javascript to andles the forms .submit() event to update the hidden input value for Filter (note I have also assumed that you want at least one checkbox selected if the value of Keyword is not null)
<script>
var checkboxes = $('input:checkbox');
var keyword = $('#Keyword');
$('form').submit(function () {
var filter = 0;
// validate at least one checkbox must be checked if Keyword has a value
if (keyword.val() && checkboxes.filter(':checked').length == 0) {
$('#filtererror').show();
return false;
}
$.each(checkboxes, function () {
if ($(this).is(':checked')) {
filter += Number($(this).val());
}
// disable checkboxes to prevent their value being added to the query string
$(this).prop('disabled', true);
})
$('#Filter').val(filter);
})
checkboxes.click(function () {
if (keyword.val() && checkboxes.filter(':checked').length == 0) {
$('#filtererror').show();
} else {
$('#filtererror').hide();
}
})
</script>
Your URL (based on ResolutionYear and ResolutionText being checked) will now be
.../Index?Keyword=someText&Filter=5
instead of
.../Index?Keyword=someText&IsResYearChecked=true&IsResYearChecked=false&IsResNumChecked=false&IsResTextChecked=true&IsResTextChecked=false

how to handle Edit Post on multiple images Asp.Net MVC

hi every one first time to ask here. So here is my Create post method on multiple images plus other data entries and it works with out any problem and it uploads all images and the content. My question is how to handle edit Post controller on multiple images.
here is the Create controller
public ActionResult SaveDataAdded([Bind(Include = "SId,Category,SName,LocalName,CommonName,Description,PicTakenBy,ContentBy,EditedBy")]SpeciesDataTable ARow, HttpPostedFileBase file,HttpPostedFileBase file2, HttpPostedFileBase file3, HttpPostedFileBase file4)
{if (ModelState.IsValid)
{
if (file != null && file.ContentLength > 0)
{
using (var Bnryreader = new System.IO.BinaryReader(file.InputStream))
{
ARow.MainPic = Bnryreader.ReadBytes(file.ContentLength);
}
}
if (file2 != null && file2.ContentLength > 0)
{
using (var reader = new System.IO.BinaryReader(file2.InputStream))
{
ARow.SecPic = reader.ReadBytes(file2.ContentLength);
}
}
if (file3 != null && file3.ContentLength > 0)
{
using (var reader = new System.IO.BinaryReader(file3.InputStream))
{
ARow.ThirdPic = reader.ReadBytes(file3.ContentLength);
}
}
if (file4 != null && file4.ContentLength > 0)
{
using (var reader = new System.IO.BinaryReader(file4.InputStream))
{
ARow.FourthPic = reader.ReadBytes(file4.ContentLength);
}
}
db.SpeciesDataTables.Add(ARow);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(ARow);
}
and here is the code block for edit. Probably my approach was not correct. But i need help on this.
public ActionResult EditSpeciesPost([Bind(Include = "SId,Category,SName,LocalName,CommonName,Description,PicTakenBy,ContentBy,EditedBy")]SpeciesDataTable Editor, HttpPostedFileBase file, HttpPostedFileBase file2, HttpPostedFileBase file3, HttpPostedFileBase file4)
{
if (file != null && file.ContentLength > 0)
{
using (var reader = new System.IO.BinaryReader(file.InputStream))
{
Editor.MainPic = reader.ReadBytes(file.ContentLength);
}
}
if (file2 != null && file2.ContentLength > 0)
{
using (var reader = new System.IO.BinaryReader(file2.InputStream))
{
Editor.SecPic = reader.ReadBytes(file2.ContentLength);
}
}
if (file3 != null && file3.ContentLength > 0)
{
using (var reader = new System.IO.BinaryReader(file3.InputStream))
{
Editor.ThirdPic = reader.ReadBytes(file3.ContentLength);
}
}
if (file4 != null && file4.ContentLength > 0)
{
using (var reader = new System.IO.BinaryReader(file4.InputStream))
{
Editor.FourthPic = reader.ReadBytes(file4.ContentLength);
}
}
db.Entry(Editor).State = EntityState.Modified;
db.SaveChanges();
//return RedirectToAction("Index");
return View(Editor);
}
here is the view for edit
#using(Html.BeginForm("EditSpecies", "Species", null, FormMethod.Post, new { enctype = "multipart/form-data" }))
{
#Html.AntiForgeryToken()
<div class="col-md-10">
#if (Model.MainPic != null) // to view first image
{
var base64 = Convert.ToBase64String(Model.MainPic);
var imgsrc = string.Format("data:image/jpg;base64,{0}", base64);
<img src='#imgsrc' style="max-width:100px;max-height:100px" />
}
</div>
<div class="col-md-10">
<input type="file" name="file" /> // input for first image
</div>
<div class="col-md-10">
#if (Model.SecPic != null)// to view second image
{
var base64 = Convert.ToBase64String(Model.SecPic);
var imgsrc = string.Format("data:image/jpg;base64,{0}", base64);
<img src='#imgsrc' style="max-width:50px;max-height:50px" />
}
</div>
<div class="col-md-10">
<input type="file" name="file2" /> input for third image
</div>
// and it continues to third and fourth picture
}
What you should be doing is, sending only null for the images user has not updated in the UI and post the data to the httppost action method where you read the existing entity and update only those properties needed to update.
I would create a view model for my view with properties needed for the view.
public class SpeciesVm
{
public int Id { set; get; }
public string Name { set; get; }
public string LocalName { set; get; }
public HttpPostedFileBase MainImg { set; get; }
public HttpPostedFileBase SecondImg { set; get; }
public string MainPicImgSrc { set; get; }
public string SecondPicImgSrc { set; get; }
}
Now in your GET action, you create an object of this, load the property values.
public ActionResult Edit(int id)
{
var e=db.SpeciesDataTables.Find(id);
var vm = new SpeciesVm() { Id=id , Name =e.SName };
vm.LocalName= e.LocalName;
if(e.MainPic!=null)
{
vm.MainPicImgSrc = $"data:image/jpg;base64,{Convert.ToBase64String(e.MainPic)}";
}
if(e.SecPic!=null)
{
vm.SecondPicImgSrc = $"data:image/jpg;base64,{Convert.ToBase64String(e.SecPic)}";
}
return View(vm);
}
Now in your view, you will use the properties of type HttpPostedFileBase for the file input
#model SpeciesVm
#using (Html.BeginForm("Edit","Species", FormMethod.Post,
new { enctype = "multipart/form-data" }))
{
#Html.HiddenFor(a=>a.Id)
#Html.HiddenFor(a=>a.Name)
#Html.HiddenFor(a=>a.LocalName)
<div class="col-md-10">
#if (!string.IsNullOrEmpty(Model.MainPicImgSrc))
{
<img src='#Model.MainPicImgSrc' />
}
<input type="file" name="MainImg" />
</div>
<div class="col-md-10">
#if (!string.IsNullOrEmpty(Model.SecondPicImgSrc))
{
<img src='#Model.SecondPicImgSrc' />
}
<input type="file" name="SecondImg" />
</div>
<button type="submit">Save</button>
}
Now in your HttpPost action method, you will use the same view model as the parameter, read the Id property value, using which read existing entity and update the properties as needed.
[HttpPost]
public ActionResult Edit(SpeciesVm model)
{
var e=db.SpeciesDataTables.Find(id);
e.SName = model.Name;
//Update the image properties only if it was send from the form
if(model.MainImg!=null)
{
e.MainPic = GetByteArrayFromImage(model.MainImg);
}
if(model.MainImg!=null)
{
e.SecPic = GetByteArrayFromImage(model.SecondImg);
}
db.SaveChanges();
return RedirectToAction("Index");
}
private byte[] GetByteArrayFromImage(HttpPostedFileBase file)
{
if (file == null)
return null;
var target = new MemoryStream();
file.InputStream.CopyTo(target);
return target.ToArray();
}

ASP.NET MVC - CheckBox List ID, Name, and Value

I'm working on an ASP.NET MVC app. In this app, I'm am working to create a checkbox list on top of a custom object. The custom object looks like this:
public class ListItem<TId, TLabel, TIsChecked>
{
private TId id;
private TLabel label;
private TIsChecked isChecked;
public ListItem(TId id, TLabel label, TIsChecked isChecked)
{
this.id = id;
this.label = label;
this.isChecked = isChecked;
}
public TId Id
{
get { return id; }
}
public TLabel Label
{
get { return label; }
}
public TIsChecked IsChecked
{
get { return isChecked; }
}
}
I have a model that looks like this:
public class MyModel
{
public IEnumerable<ListItem<Guid, string, bool>> Options { get; set; }
}
I then have a controller, with an action called Profile, that looks like this:
[HttpGet]
public ActionResult Profile()
{
var model = new MyModel();
return View(model);
}
[HttpPost]
public ActionResult Profile(MyModel model)
{
return View(model);
}
I am rendering my checkbox options in my Razor view like this:
#foreach (var option in Model.Options)
{
<div class="form-group">
<div class="checkbox">
<label>
<input name="Options" value="#option.Id" type="checkbox" #((option.IsChecked == true) ? "checked" : string.Empty)> #option.Label
</label>
</div>
</div>
}
The options render fine. But, when I save the form, it looks like only the first selected checkbox ID is returned. I'm not sure how to a) properly render my HTML for a checkbox list and b) get the values to properly post back to the server. I've seen other examples in blog posts, but, my view will have significantly more HTML. For that reason, I'm trying to figure out "how" checkbox values are rendered on the client-side and posted back to the server in ASP.NET MVC.
First you have to do change in your model to use List<T> instead of IEnumerable<T>:
public List<ListItem<Guid, string, bool>> Options { get; set; }
Secondly, you do not have setters for your properties, so data would not be posted back, add setters:
public TId Id
{
get { return id; }
set { id = value;}
}
public TLabel Label
{
get { return label; }
set { label = value;}
}
public TIsChecked IsChecked
{
get { return isChecked; }
set { isChecked = value;}
}
and at last you simply need to use strongly typed html helper and use for loop so that indexing can bind data for post:
#for(int i=0; i< Model.Options.Count; i++)
{
<div class="form-group">
<div class="checkbox">
<label>
#Html.CheckBoxFor(model => model.Options[i].IsChecked) #Model.Options[i].Label
</label>
#Html.HiddenFor(model=> model.Options[i].Id)
#Html.HiddenFor(model=> model.Options[i].Label)
</div>
</div>
}
This will create check-boxes with correct names so that they can be binded on post.
You might want to have a look on How List binding works

Resources