We have to combine multiple properties into one EditorFor field in a Razor view.
We have the properties Quantity, UnitOfMeasure and Ingredient. These need to be combined so the user can just type what he or she needs i.e. 10 kg potatoes, instead of entering the information into multiple fields.
Once this is done, we also need autocomplete on the UOM and ingredient properties.
I have created a partial view for this code.
#model IEnumerable<RecipeApplication.Models.RecipeLine>
<div class="form-group">
#Html.Label("Ingrediënten", htmlAttributes: new { #class = "control-label col-md-2" })
<div>
#foreach (var item in Model)
{
<p>
#Html.EditorFor(modelItem => item.Quantity, new { htmlAttributes = new { #class = "form-control-inline" } })
#Html.EditorFor(modelItem => item.UnitOfMeasure.Abbreviation, new { htmlAttributes = new { #class = "form-control-inline" } })
#Html.EditorFor(modelItem => item.Ingredient.Name, new { htmlAttributes = new { #class = "form-control-inline" } })
</p>
}
</div>
</div>
Obviously this is not the intention.
And this is the code for the Edit functions:
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
RecipeModel recipeModel = db.Recipes.Find(id);
if (recipeModel == null)
{
return HttpNotFound();
}
GetRecipeLines(id);
return View(recipeModel);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "Name,Description,ImageUrl")] RecipeModel recipeModel, int?id)
{
if (ModelState.IsValid)
{
db.Entry(recipeModel).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
GetRecipeLines(id);
return View(recipeModel);
}
I have looked on Google and StackOverflow but I can't find a proper answer to get this done.
Personally I wouldn't even know where to start at this moment.
I hope someone can help figuring this out.
Thanks.
Add a new getter property on ReceipLine
C# 6.0 Syntax:
public string QuantityUomIngredient =>
$"{Quantity} {UnitOfMeasure?.Abbreviation ?? ""} {Ingredient?.Name ?? ""}";
Then your view should look like this
#Html.EditorFor(modelItem => item.QuantityUomIngredient ...
And then build a custom model binder to parse QuantityUomIngredient into its corresponding properties (this part should be fun to implement). But be sure to do a good validation on the input so you have good data to parse.
Thanks for the anwer Leo Nix, it surely put me into the right direction.
Here is the code I wrote so far and it seems to work like a charm. (I did not include error handling yet.)
public class RecipeLine
{
[Key]
public int RecipeLineId { get; set; }
public int RecipeId { get; set; }
public double Quantity { get; set; }
public virtual UnitOfMeasureModel UnitOfMeasure { get; set; }
public virtual IngredientModel Ingredient { get; set; }
public string QuantityUomIngredient => $"{Quantity} {UnitOfMeasure?.Abbreviation ?? ""} {Ingredient?.Name ?? ""}";
}
And the custom Binder I wrote. This one took quite some extra research.
class RecipeLineCustomBinder : DefaultModelBinder
{
private RecipeApplicationDb db = new RecipeApplicationDb();
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
HttpRequestBase request = controllerContext.HttpContext.Request;
// Get the QuantityCustomIngredient from the webform.
string quantityUomIngredient = request.Form.Get("QuantityUomIngredient");
// Get the IngredientID from the webform.
int recipeID = int.Parse(request.Form.Get("RecipeId"));
// Split the QuantityCustomIngredient into seperate strings.
string[] quantityUomIngredientArray = quantityUomIngredient.Split();
//string[] quantityUomIngredientArray = quantityUomIngredient.Split(new string[] { " " }, 2, StringSplitOptions.RemoveEmptyEntries);
if (quantityUomIngredientArray.Length >= 3)
{
// Get the quantity value
double quantityValue;
bool quantity = double.TryParse(quantityUomIngredientArray[0], out quantityValue);
// Get the UOM value.
string uom = quantityUomIngredientArray[1];
UnitOfMeasureModel unitOfMeasure = null;
bool checkUOM = (from x in db.UnitOfMeasures
where x.Abbreviation == uom
select x).Count() > 0;
if (checkUOM)
{
unitOfMeasure = (from x in db.UnitOfMeasures
where x.Abbreviation == uom
select x).FirstOrDefault();
}
// Get the ingredient out of the array.
string ingredient = "";
for (int i = 2; i < quantityUomIngredientArray.Length; i++)
{
ingredient += quantityUomIngredientArray[i];
if (i != quantityUomIngredientArray.Length - 1)
{
ingredient += " ";
}
}
bool checkIngredient = (from x in db.Ingredients where x.Name == ingredient select x).Count() > 0;
IngredientModel Ingredient = null;
if (checkIngredient)
{
Ingredient = (from x in db.Ingredients
where x.Name == ingredient
select x).FirstOrDefault();
}
// Return the values.
return new RecipeLine
{
Quantity = quantityValue,
UnitOfMeasure = unitOfMeasure,
Ingredient = Ingredient,
RecipeId = recipeID
};
}
else
{
return null;
}
}
}
In the Razor view this is the code that I used:
<div class="form-group">
#Html.LabelFor(model => model.QuantityUomIngredient, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.QuantityUomIngredient, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.QuantityUomIngredient, "", new { #class = "text-danger" })
</div>
</div>
I added the custom binder in Global.asax.cs
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
ModelBinders.Binders.Add(typeof(RecipeLine), new RecipeLineCustomBinder());
}
}
And finally added the custom binder to the controller
[HttpPost]
public ActionResult Create([ModelBinder(typeof(RecipeLineCustomBinder))] RecipeLine recipeLine)
{
if (ModelState.IsValid)
{
db.RecipeLines.Add(recipeLine);
db.SaveChanges();
return RedirectToAction("Index", new { id = recipeLine.RecipeId });
}
return View(recipeLine);
}
I hope this will help other developers as well.
Related
This question already has answers here:
What is a NullReferenceException, and how do I fix it?
(27 answers)
Closed 4 years ago.
This model is used to define a view:
namespace OnlineStore.ViewModels
{
public class SubCategoryVM
{
[Key]
public int ID { get; set; }
[Required]
public virtual string Name { get; set; }
[Required(ErrorMessage = "Parent Category Name is required")]
public virtual string ParentName { get; set; }
public IEnumerable<SelectListItem> categoryNames { get; set; }
}
}
Inside controller:
public ActionResult createSubCategory()
{
SubCategoryVM model = new SubCategoryVM();
var cNames = db.Categories.ToList();
model.categoryNames = cNames.Select(x
=> new SelectListItem
{
Value = x.Name,
Text = x.Name
});
return View(model);
}
[HttpPost]
public ActionResult createSubCategory(int? id, SubCategoryVM model)
{
SubCategory sc = new SubCategory();
if (ModelState.IsValid)
{
sc.ParentName = model.ParentName;
sc.Name = model.Name;
}
return View();
}
and View:
#model OnlineStore.ViewModels.SubCategoryVM
<div class="form-group">
#Html.LabelFor(model => model.ParentName, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownListFor(model => model.ParentName, Model.categoryNames, "--Please select an option--", new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.ParentName, "", new { #class = "text-danger" })
</div>
This code is throwing a null-reference exception on line #Html.DropDownListFor(model => model.ParentName, Model.categoryNames, "--Please select an option--", new { #class = "form-control" }) saying:
Model.categoryName (Object reference not set to an instance of an object).
Please help me debug it.
Thanks in advance.
Problem is when you are posting the form and returning the View with the form data in case of invalid form, categoryNames in the model is becoming null and you have to repopulate the categoryNames before returning the view with model again.
So update your createSubCategory post method as follows:
[HttpPost]
public ActionResult createSubCategory(int? id, SubCategoryVM model)
{
SubCategory sc = new SubCategory();
if (ModelState.IsValid)
{
sc.ParentName = model.ParentName;
sc.Name = model.Name;
}
var cNames = db.Categories.ToList();
model.categoryNames = cNames.Select(x
=> new SelectListItem
{
Value = x.Name,
Text = x.Name
});
return View(model);
}
I have a people controller for user management and I'm trying to figure out how to get the dropdown choice when the user submits from the edit page. Whenever I hit submit on the page, none of the values from the view model seem to carry through to the post. I can't get the value they chose from the drop down to set the role.
See view model below:
public class PersonViewModel
{
public int PersonId { get; set; }
[Display(Name = "Full Name")]
public string FullName { get; set; }
public string Email { get; set; }
[Display(Name = "Current Role")]
public string SetRole { get; set; }
public List<RoleListViewModel> Roles { get; set; }
}
See controller edit functions below:
// GET: People/Edit/5
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Person person = db.people.Find(id);
if (person == null)
{
return HttpNotFound();
}
PersonViewModel pvm = new PersonViewModel();
List<IdentityRole> roles = adb.Roles.ToList();
var rlvm = new List<RoleListViewModel>();
roles.ForEach(x => rlvm.Add(new RoleListViewModel { RoleId = x.Id, RoleName = x.Name }));
pvm.PersonId = person.PersonId;
pvm.FullName = person.FirstName + " " + person.LastName;
pvm.Email = person.Email;
pvm.Roles = rlvm;
ViewBag.RoleList = new SelectList(rlvm, "RoleName", "RoleName", person.CurrentRole);
return View(pvm);
}
// POST: People/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(PersonViewModel pvm)
{
if (ModelState.IsValid)
{
db.Entry(pvm).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
var usr = new AccountController();
var pers = db.people.Where(x => x.PersonId == pvm.PersonId).FirstOrDefault();
usr.UserManager.AddToRoleAsync(pers.NetId, /* their choice should go here but how? */);
db.SaveChanges();
return View(pvm);
}
Here is the cshtml:
<div class="form-group">
#Html.LabelFor(model => model.Roles, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="form-control-static">
#Html.DropDownList("RoleList", null, new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.Roles, "", new { #class = "text-danger" })
</div>
</div>
Make a variable in your View Model to store the selected value and then in the view use
#Html.DropDownListFor(m => m.SelectedRoleVariable, RolesSelectList, new { #class = "form-control" });
I have this model with required properties. Yes they are required for creating an item, but when I want to update a single column of them, I get this error about required fields. So it seems that my controller wants to update all of them.
In this case there's an item for sale and one can make an offer for it.
Part of my model:
public class Ad
{
public int AdID { get; set; }
[Required()]
[Display(Name = "Otsikko")]
public string Title { get; set; }
[Required()]
[DataType(DataType.MultilineText)]
[AllowHtml]
[Display(Name = "Kuvaus")]
public string Text { get; set; }
[Required()]
[DataType(DataType.Currency)]
[Display(Name = "Hinta")]
public float Price { get; set; }
[DataType(DataType.Currency)]
[Display(Name = "Tarjottu")]
public float Offer { get; set; }
My controller:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Offer([Bind(Include = "Offer")] Ad ad)
{
if (ModelState.IsValid)
{
db.Entry(ad).State = EntityState.Modified;
db.SaveChanges();
return RedirectToRoute("ilmoitus", new { id = ad.AdID });
}
else
{
var message = string.Join(" | ", ModelState.Values
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage));
return new HttpStatusCodeResult(HttpStatusCode.BadRequest, message);
}
}
Part of view:
#using (Html.BeginForm("Offer", "Ads"))
{
#Html.AntiForgeryToken()
#Html.HiddenFor(model => model.AdID)
#Html.EditorFor(model => model.Offer, new { htmlAttributes = new { #class = "form-control offer-input", #placeholder = "Tarjoa..." } })
<button class="btn-default btn offer-btn">Tarjoa {{offer}} €</button>
}
I've tried this with same error How to Update only single field using EF
I got it working like this, included all the other properties as hidden and bound them in the controller:
View:
#using (Html.BeginForm("Offer", "Ads"))
{
#Html.AntiForgeryToken()
#Html.HiddenFor(model => model.AdID)
#Html.HiddenFor(model => model.Title)
#Html.HiddenFor(model => model.Text)
#Html.HiddenFor(model => model.Price)
#Html.HiddenFor(model => model.Location)
#Html.HiddenFor(model => model.PaymentOptions)
#Html.HiddenFor(model => model.DeliveryOptions)
#Html.HiddenFor(model => model.SellerEmail)
#Html.EditorFor(model => model.Offer, new { htmlAttributes = new { #class = "form-control offer-input", #placeholder = "Tarjoa..." } })
<button class="btn-default btn offer-btn">Tarjoa {{offer}} €</button>
}
Controller:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Offer([Bind(Include = "AdID,Title,Text,Price,Offer,Location,PaymentOptions,DeliveryOptions,SellerEmail")] Ad ad)
{
if (ModelState.IsValid)
{
db.Entry(ad).State = EntityState.Modified;
db.SaveChanges();
return RedirectToRoute("ilmoitus", new { id = ad.AdID });
}
else
{
var message = string.Join(" | ", ModelState.Values
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage));
return new HttpStatusCodeResult(HttpStatusCode.BadRequest, message);
}
}
Is this a security issue though?
I have two solution for the above query.
You can use two different models for updating and insertion example
Remove the properties from ModelState then TryUpdateModel
public ActionResult UpdateAd(Ad ad)
{
ModelState.Remove("Title");
ModelState.Remove("Text");
ModelState.Remove("Price");
var p = GetAd();
if (TryUpdateModel(p))
{
//Save Changes;
}
}
What I'm trying to do...
I am trying to get a newly added item to display in a cascading dropdownlist.
Overview...
The first dropdownlist (I'll call it ddlCategory) is for selecting a Category of electrical devices (ie. Appliances, Audio-Visual, Lighting, etc.). The second dropdownlist (I'll call ddlElecDev) is populated with the devices which are filtered by the selected Category. If the device isn't listed in ddlElecDev then the user can click a link to add a new one. After saving the newly added electrical device, the user is redirected back to the original page with the electrical device id as a parameter.
All the above seems to work fine. However, when the user is redirected to the first page, not only is the newly added electrical device not selected in ddlElecDev, but it doesn't even appear in the list. Strangely, if I refresh the page, it is automatically selected.
Can anyone explain to me how to get the newly added device to be selected without having to refresh the page?
Here's the markup for selecting the category and electrical device:
<div class="form-group">
#Html.LabelFor(model => model.SelectedCategory, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownListFor(model => model.SelectedCategory, Model.Categories, "Select a Category", new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.SelectedCategory, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.ElectricalDeviceID, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.CascadingDropDownListFor(
expression: model => model.ElectricalDeviceID,
triggeredByProperty: model => model.SelectedCategory,
url: Url.Action("GetElectricalDevices", "ElectricalDeviceConfigurations"),
ajaxActionParamName: "categoryId",
optionLabel: "Select an Electrical Device",
disabledWhenParrentNotSelected: false,
htmlAttributes: new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.ElectricalDeviceID, "", new { #class = "text-danger" })
#Html.ActionLink("Not Listed? Add a new Electrical Device", "AddNew", "ElectricalDevices", new { returnUrl = string.Format("/ElectricalDeviceConfigurations/AddConfiguration?eventVendorId={0}", Model.EventVendorID) }, null)
</div>
</div>
The documentation for the Cascading DropDownList Helper can be found at
https://github.com/alexanderar/Mvc.CascadeDropDown
Here's the Controller method for selecting the Electrical Device:
public ActionResult AddConfiguration(int? eventVendorId, int? newElecDev)
{
(Some code removed for brevity)
var selectedCategoryId = db.ElectricalDeviceCategoryLookups.Where(cat => cat.Category == Enums.ElectricalDeviceCategory.All).FirstOrDefault().ID;
var electricalDeviceID = (newElecDev.HasValue) ? newElecDev : null;
return View(new ElecDevConfigSelectionViewModel { EventVendorID = eventVendorId, EventVendor = eventVendor, Categories = GetCategories(), SelectedCategory = selectedCategoryId, ElectricalDeviceID = electricalDeviceID });
}
Here's the controller methods for populating the Categories and the Electrical Devices:
private List<SelectListItem> GetCategories()
{
var categories = new List<SelectListItem>();
db.ElectricalDeviceCategoryLookups.OrderBy(c => c.Description).ToList().ForEach(item => categories.Add(new SelectListItem { Text = item.Description, Value = item.ID.ToString() } ));
return categories;
}
public ActionResult GetElectricalDevices(int? categoryId)
{
if (categoryId.HasValue)
{
var selCategory = db.ElectricalDeviceCategoryLookups.Where(cat => cat.ID == categoryId).FirstOrDefault().Category;
var elecDevicesSelectList = new List<SelectListItem>();
var elecDevices = (selCategory == Enums.ElectricalDeviceCategory.All) ? db.ElectricalDevices.OrderBy(ed => ed.Name).ToList() : db.ElectricalDevices.Where(ed => ed.Category == selCategory).OrderBy(ed => ed.Name).ToList();
elecDevices.ForEach(ed => elecDevicesSelectList.Add(new SelectListItem { Text = ed.Name, Value = ed.ID.ToString() }));
return Json(elecDevicesSelectList, JsonRequestBehavior.AllowGet);
}
return null;
}
Here's the View Model:
public class ElecDevConfigSelectionViewModel
{
public int? EventVendorID { get; set; }
public EventVendor EventVendor { get; set; }
[Display(Name = "Category")]
public int SelectedCategory { get; set; }
public IList<SelectListItem> Categories { get; set; }
[Display(Name = "Electrical Device")]
public int? ElectricalDeviceID { get; set; }
}
Here's the controller methods for adding a new electrical device:
// GET: ElectricalDevices/AddNew
public ActionResult AddNew(string returnUrl)
{
ViewBag.ReturnUrl = returnUrl;
var selectedCategoryId = db.ElectricalDeviceCategoryLookups.FirstOrDefault().ID;
return View(new AddNewElectricalDeviceViewModel { ReturnUrl = returnUrl, Categories = GetCategories(), SelectedCategory = null });
}
// POST: ElectricalDevices/AddNew
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AddNew(AddNewElectricalDeviceViewModel model)
{
if (ModelState.IsValid)
{
var selectedCategory = db.ElectricalDeviceCategoryLookups.Find(model.SelectedCategory);
var electricalDevice = new ElectricalDevice
{
Name = model.ElectricalDevice.Name,
Description = model.ElectricalDevice.Description,
Category = selectedCategory.Category,
Wattage = model.ElectricalDevice.Wattage
};
db.ElectricalDevices.Add(electricalDevice);
db.SaveChanges();
var returnUrl = string.Format("{0}&newElecDev={1}", model.ReturnUrl, electricalDevice.ID);
return RedirectToLocal(returnUrl);
}
return View();
}
Here's the View Model for adding a new electrical device:
public class AddNewElectricalDeviceViewModel
{
public string ReturnUrl { get; set; }
[Display(Name = "Category")]
public int? SelectedCategory { get; set; }
public IList<SelectListItem> Categories { get; set; }
public ElectricalDevice ElectricalDevice { get; set; }
}
I'm working on a webapp for work, and I'm using standard CRUD style interactions. However, there are certain fields that I do not want the users updating, so I removed them from the view. However, if I don't explicitly set these fields, they're cleared when the model is updated in the database.
I'm concerned with what the proper method of populating the fields for my ViewModels is.
The rough idea I came up with was something like this:
My view model:
public class EditSoftwareTrackingViewModel
{
public EditSoftwareTrackingViewModel(SoftwareTracking model)
{
Id = model.Id;
SoftwareId = model.SoftwareId;
ComputerId = model.ComputerId;
SoftwareActionId = model.SoftwareActionId;
LastModified = model.LastModified;
Computer = model.Computer;
Software = model.Software;
SoftwareAction = model.SoftwareAction;
}
public int Id { get; set; }
[DisplayName("Software")]
public int SoftwareId { get; set; }
[DisplayName("Computer")]
public int ComputerId { get; set; }
[DisplayName("Software Action")]
public int SoftwareActionId { get; set; }
[DisplayName("Last Modified")]
public DateTime? LastModified { get; set; }
public virtual Computer Computer { get; set; }
public virtual Software Software { get; set; }
public virtual SoftwareAction SoftwareAction { get; set; }
}
My main model
[Table("asset.SoftwareTracking")]
public partial class SoftwareTracking
{
public int Id { get; set; }
[DisplayName("Software")]
public int SoftwareId { get; set; }
[DisplayName("Computer")]
public int ComputerId { get; set; }
[DisplayName("Date Entered")]
public DateTime? EnteredDate { get; set; }
[DisplayName("Software Action")]
public int SoftwareActionId { get; set; }
[DisplayName("Last Modified")]
public DateTime? LastModified { get; set; }
public virtual Computer Computer { get; set; }
public virtual Software Software { get; set; }
public virtual SoftwareAction SoftwareAction { get; set; }
}
And my controller using the view model
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
EditSoftwareTrackingViewModel softwaretracking = new EditSoftwareTrackingViewModel(db.SoftwareTrackings.Find(id));
if (softwaretracking == null)
{
return HttpNotFound();
}
GeneratePageData(softwaretracking.Software.Id);
return View(softwaretracking);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(EditSoftwareTrackingViewModel softwaretracking)
{
if (ModelState.IsValid)
{
softwaretracking.LastModified = DateTime.Now;
var softwareTrack = db.SoftwareTrackings.Find(softwaretracking.Id);
softwareTrack = new SoftwareTracking
{
Computer = softwaretracking.Computer,
ComputerId = softwaretracking.ComputerId,
LastModified = softwaretracking.LastModified,
Software = softwaretracking.Software,
SoftwareAction = softwaretracking.SoftwareAction,
SoftwareActionId = softwaretracking.SoftwareActionId,
SoftwareId = softwaretracking.SoftwareId,
EnteredDate = softwareTrack.EnteredDate
};
db.Entry(softwareTrack).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
GeneratePageData(softwaretracking.Software.Id);
return View(softwaretracking);
}
Is there a better alternative? Or should I continue to create my view models in this manner?
EDIT
My business logic and view
private void GeneratePageData(int? id = null)
{
ViewBag.Computers = new SelectList(db.Computers, "Id", "ComputerName");
ViewBag.SoftwareActions = new SelectList(db.SoftwareActions, "Id", "ActionPerformed");
var usedSoft = (from softTrack in db.SoftwareTrackings
where (softTrack.SoftwareActionId != 3)
select softTrack.Software);
var softwareList = (from soft in db.Softwares
where (
((from softTrack in db.SoftwareTrackings
where (softTrack.SoftwareActionId != 3 && softTrack.SoftwareId == soft.Id)
select softTrack.Software).Count() < soft.KeyQuantity)
&& !(soft.AssetStatusId == 4 || soft.AssetStatusId == 5)
|| soft.Id == id)
select soft).ToList();
ViewBag.SoftwareList = softwareList.Select(t => new SelectListItem
{
Text = t.SoftwareIdNameFull,
Value = t.Id.ToString()
});
}
And my view
#model Lighthouse_Asset_Manager.Models.EditSoftwareTrackingViewModel
#{
ViewBag.Title = "Edit Software Install";
Layout = "";
}
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">
×
</button>
<h4 class="modal-title" id="myModalLabel">Edit Software Install</h4>
</div>
<div class="modal-body">
#using (Html.BeginForm(null, null, FormMethod.Post, new { id = "computerForm" }))
{
#Html.AntiForgeryToken()
#Html.HiddenFor(model => model.Id)
<div class="form-horizontal">
#Html.ValidationSummary(true)
<div class="form-group">
#Html.LabelFor(model => model.SoftwareId, "Software", new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownList("SoftwareId", (IEnumerable<SelectListItem>)ViewBag.SoftwareList, "-- Select --", new
{
#style = "width:100%",
#class = "select2"
})
#Html.ValidationMessageFor(model => model.SoftwareId)
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.ComputerId, "Computer", new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownList("ComputerId", (IEnumerable<SelectListItem>)ViewBag.Computers, "-- Select --", new
{
#style = "width:100%",
#class = "select2"
})
#Html.ValidationMessageFor(model => model.ComputerId)
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.SoftwareActionId, "Action", new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownList("SoftwareActionId", (IEnumerable<SelectListItem>)ViewBag.SoftwareActions, "-- Select --", new
{
#style = "width:100%",
#class = "form-control"
})
#Html.ValidationMessageFor(model => model.SoftwareActionId)
</div>
</div>
<div class="form-actions no-color">
<button type="submit" class="btn btn-primary btn-sm"><i class="fa fa-floppy-o"></i> Edit Install Record</button>
<button type="button" class="btn btn-default" data-dismiss="modal">
Cancel
</button>
</div>
</div>
}
</div>
You approach of using a view model is a good one. The answers to this question explains some of the benefits including preventing over-posting attacks, using view specific display and validation attributes and including view specific properties such as SelectLists. Tools such as automapper can make it easy to map between you data and view models and reduce the code in the controller. A few changes I would suggest to your view model. The LastModified, Computer, Software and SoftwareAction properties are not required (you not binding to these), and I would include the SelectList properties in the model rather than ViewBag
View model
public class EditSoftwareTrackingViewModel
{
public int Id { get; set; }
[Display(Name="Software")]
public int SoftwareId { get; set; }
[Display(Name="Computer")]
public int ComputerId { get; set; }
[Display(Name="Software Action")]
public int SoftwareActionId { get; set; }
public SelectList Computers { get; set; }
public SelectList SoftwareActions{ get; set; }
public SelectList SoftwareList{ get; set; }
}
Then change the GeneratePageData() method to accept the view model
private void GeneratePageData(EditSoftwareTrackingViewModel model)
{
model.Computers = new SelectList(db.Computers, "Id", "ComputerName");
....
and in the view (always preferable to use the strongly typed helpers)
#Html.DropDownListFor(m => m.SoftwareId, Model.SoftwareList, "-- Select --", new { #class = "select2" })
A few other things to note.
You should use the [Display(Name="..")] attribute (not
[DisplayName(..)])
When you set the LastModified property, you should consider using
UCT time.
The hidden input for the Id property is not required in the view
(assuming your using the default {controller}/{action}/{id} route
mapping) - its added to the route values and will be bound anyway
Unless you specifically want an id attribute for the form, you can
just use #using(Html.BeginForm()) {
You do not need the second parameter in LabelFor() - it can be just
Html.LabelFor(m => m.SoftwareId, new { #class = "control-label
col-md-2" }) since you have specified it in the [Display]
attribute
Finally, if you want to simplify your view further, you could consider custom EditorTemplates or html helpers as indicated in this answer which would allow you to replace
<div class="form-group">
#Html.LabelFor(model => model.SoftwareId, new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownListFor(m => m.SoftwareId, Model.SoftwareList, "-- Select --", new { #class = "select2" })
#Html.ValidationMessageFor(model => model.SoftwareId)
</div>
</div>
with (custom EditorTemplate)
#Html.EditorFor(m => m.SoftwareId, "BootstrapSelect", Model.SoftwareList)
or (custom HtmlHelper)
#Html.BootstrapDropDownFor(m => m.SoftwareId, Model.SoftwareList)
You should use the AutoMapper to make the mapping between Model and ViewModel cleaner. Use this code to create the mapper first.
Mapper.CreateMap<SoftwareTracking, EditSoftwareTrackingViewModel>();
Mapper.CreateMap<EditSoftwareTrackingViewModel, SoftwareTracking>();
When you want to create a viewmodel from model, do this:
public ActionResult Edit(int? id)
{
SoftwareTracking tracking = db.SoftwareTrackings.Find(id);
EditSoftwareTrackingViewModel viewmodel =
Mapper.Map<SoftwareTracking, EditSoftwareTrackingViewModel>(tracking);
return View(viewmodel);
}
When you want to populate the info from the viewmodel back to the model, do this
public ActionResult Edit(EditSoftwareTrackingViewModel vm)
{
if (ModelState.IsValid)
{
vm.LastModified = DateTime.Now;
var softwareTrack = db.SoftwareTrackings.Find(softwaretracking.Id);
softwareTrack =
Mapper.Map<EditSoftwareTrackingViewModel, SoftwareTracking>(vm, softwareTrack);
db.Entry(softwareTrack).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
To patch update your model without loading the object from Db. Try Attach:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(EditSoftwareTrackingViewModel softwaretracking)
{
if (ModelState.IsValid)
{
var softwareTrack = new SoftwareTracking
{
Computer = softwaretracking.Computer,
ComputerId = softwaretracking.ComputerId,
LastModified = softwaretracking.LastModified,
Software = softwaretracking.Software,
SoftwareAction = softwaretracking.SoftwareAction,
SoftwareActionId = softwaretracking.SoftwareActionId,
SoftwareId = softwaretracking.SoftwareId,
EnteredDate = softwareTrack.EnteredDate
};
db.SoftwareTrackings.Attach(softwareTrack);
db.Entry(softwareTrack).Property(a => a.Computer).IsModified = true;
db.Entry(softwareTrack).Property(a => a.ComputerId).IsModified = true;
db.Entry(softwareTrack).Property(a => a.LastModified).IsModified = true;
db.Entry(softwareTrack).Property(a => a.Computer).IsModified = true;
db.Entry(softwareTrack).Property(a => a.Software).IsModified = true;
db.Entry(softwareTrack).Property(a => a.SoftwareAction).IsModified = true;
db.Entry(softwareTrack).Property(a => a.SoftwareActionId).IsModified = true;
db.Entry(softwareTrack).Property(a => a.SoftwareId).IsModified = true;
db.SaveChanges();
return RedirectToAction("Index");
}
GeneratePageData(softwaretracking.Software.Id);
return View(softwaretracking);
}
Regarding the second question about whether to use ViewModel or just use the Model directly. This is really a matter of opinion, each approach has its pros and cons. I don't have strong opinion about this, i just want to point out these pros and cons for your consideration:
Using the model directly saves us from creating the viewModel, resulting in smaller source code and avoiding mapping logic but it would mix concerns. Because you use the same Model for your domain logic and for communcating with the client, any changes to the model may propagate up to the client if we don't take that into account.
Using the viewModel is a good way for separation of concerns but it would require more effort and mapping logic (maybe slow down the performance a bit). To apply ViewModel efficiently, I suggest using a mapper: https://github.com/AutoMapper/AutoMapper/wiki/Getting-started
This is the Model Class
[Table("CURRENCY")]
public class CurrencyClass : ICurrency
{
private Int32 mCURRENCY_ID = default(Int32);
[Key]
public virtual Int32 CURRENCY_ID
{
get { return mCURRENCY_ID; }
set { mCURRENCY_ID = value; }
}
private string mCURRENCY_NAME = default(string);
public virtual string CURRENCY_NAME
{
get { return mCURRENCY_NAME;}
set { mCURRENCY_NAME = value;}
}
private string mCURRENCY_DESC = default(string);
public virtual string CURRENCY_DESC
{
get { return mCURRENCY_DESC; }
set { mCURRENCY_DESC = value; }
}
private string mCURRENCY_SYMBOLE = default(string);
public virtual string CURRENCY_SYMBOLE
{
get { return mCURRENCY_SYMBOLE; }
set { mCURRENCY_SYMBOLE = value; }
}
private Int32 mcreated_by = default(Int32);
public virtual Int32 created_by
{
get { return mcreated_by; }
set { mcreated_by = value; }
}
private DateTime mcreated_date = default(DateTime);
public virtual DateTime created_date
{
get { return mcreated_date; }
set { mcreated_date = value; }
}
private Int32 mmodified_by = default(Int32);
public virtual Int32 modified_by
{
get { return mmodified_by; }
set { mmodified_by = value; }
}
private DateTime mmodified_date = default(DateTime);
public virtual DateTime modified_date
{
get { return mmodified_date; }
set { mmodified_date = value; }
}
}
This is the ViewModel
public class CurrencyViewModel
{
[Key]
public Int32 CURRENCY_Id { get; set; }
[Required(ErrorMessage="Currency Name is required")]
public string CURRENCY_NAME { get; set; }
[Required(ErrorMessage="Currency Description is required")]
public string CURRENCY_DESC { get; set; }
[Required(ErrorMessage = "Currency Symbole is Required")]
public string CURRENCY_SYMBOLE { get; set; }
}
This is the Action
[HttpPost]
[ActionName("Create")]
public ActionResult Create(CurrencyViewModel vm)
{
if (!ModelState.IsValid)
{
return View("Create");
}
obj.CURRENCY_NAME = vm.CURRENCY_NAME;
obj.CURRENCY_DESC = vm.CURRENCY_DESC;
obj.CURRENCY_SYMBOLE = vm.CURRENCY_SYMBOLE;
obj.created_by = 1;
obj.created_date = DateTime.Now;
obj.modified_by = 1;
obj.modified_date = DateTime.Now;
db.Currencies.Add(obj);
db.SaveChanges();
return RedirectToAction("Index");
}