-1

I am using boilerplate CRUD methods, which include BIND operations on the methods that catch the form submissions. I have discovered that if I have my fields as nullable both in the model as well as in the DB, and I do not include those fields in a CRUD operation (also no presence in BIND), these fields end up null when before they were filled. If I flag these fields as not nullable in the DB, I cannot complete the CRUD operation without including these fields in hidden form fields because they are not nullable.

How do I make a CRUD operation ignore these fields without adding them as hidden fields in the forms?? As in, do not null them, do not change their data.

For example, if I have my POST method as such:

public async Task<ActionResult> Edit([Bind(Include = "CompanyId,CompanyName,CompanyAddress,CompanyCity")] Company company) {

And there is a field both in the model as well as in the DB such as CompanyCity, if it is nullable in model and db it gets nulled with the update. If it is not nullable in the model and db, the update fails because the field is not nullable but the update wants to null it because it didn't exist in the bind.

I am also using only the base models, such as Company, for this example. However when I try to make another base model, such as EditCompanyViewModel, I am unable to pull data out of the database to put into that view model. The entire await command gets flagged as being not of the correct model/type.

Essentially, I need to know how to edit only part of a table, without messing/mucking/deleting the rest of the table entries and without creating a metric arseload of hidden form fields that exist purely to hold the data I don't want to edit.

I have a conceptual gap here, and I am metaphorically chasing my tail. I can't seem to bridge the gap to a solution.

EDIT:

My modified view model:

public class EditMarketingViewModel {
  [Key]
  public Guid CompanyId { get; set; }
  [DisplayName("How did you hear of us")]
  public Guid? HowHeardId { get; set; }
  [DisplayName("eNewsletter")]
  public bool eNewsletter { get; set; }
  [DisplayName("Event Code")]
  public Guid? EventCodeId { get; set; }
  [DisplayName("Notes")]
  public string MarketingNotes { get; set; }
  #region Essentials
  [HiddenInput, Timestamp, ConcurrencyCheck]
  public byte[] RowVersion { get; set; }
  [HiddenInput]
  public DateTime Modified { get; set; }
  [HiddenInput]
  public string TouchedBy { get; set; }
  #endregion
}

My view:

@model CCS.Models.EditMarketingViewModel

@{
ViewBag.Title = "Edit Marketing Info.";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>@ViewBag.Title</h2>
@using(Html.BeginForm()) {
  @Html.AntiForgeryToken()
  @Html.ValidationSummary(true, "", new { @class = "text-danger" })
  <fieldset>
    @Html.HiddenFor(model => model.CompanyId)
    @Html.HiddenFor(model => model.RowVersion)
    @Html.LabelFor(model => model.HowHeardId, htmlAttributes: new { @class = "control-label" })@Html.DropDownList("HowHeardId", null, " « ‹ Select a How Heard Type › » ", htmlAttributes: new { @class = "form-control" })
    @Html.ValidationMessageFor(model => model.HowHeardId, "", new { @class = "text-danger" })
    @Html.LabelFor(model => model.eNewsletter, htmlAttributes: new { @class = "control-label" })@Html.EditorFor(model => model.eNewsletter, new { htmlAttributes = new { @data_on_text = "Yes", @data_off_text = "No" } })
    @Html.ValidationMessageFor(model => model.eNewsletter, "", new { @class = "text-danger" })
    @Html.LabelFor(model => model.EventCodeId, htmlAttributes: new { @class = "control-label" })@Html.DropDownList("EventCodeId", null, " « ‹ Select an Event Code › » ", htmlAttributes: new { @class = "form-control" })
    @Html.ValidationMessageFor(model => model.EventCodeId, "", new { @class = "text-danger" })
    @Html.LabelFor(model => model.MarketingNotes, htmlAttributes: new { @class = "control-label" })@Html.EditorFor(model => model.MarketingNotes, new { htmlAttributes = new { @class = "form-control" } })
    @Html.ValidationMessageFor(model => model.MarketingNotes, "", new { @class = "text-danger" })
    <input type="submit" value="Save" class="btn btn-default" />
  </fieldset>
}
<p>[ @Html.ActionLink("Back to List", "Index", "Company") ]</p>

Now how do I modify my controller to work with it:

// GET: Company/EditMarketing
public async Task<ActionResult> EditMarketing() {
  var id = new Guid(User.GetClaimValue("CWD-Company"));
  Company company = await db.Company.FindAsync(id);
  if(company == null) {
    return HttpNotFound();
  }
  ViewBag.HowHeardId = new SelectList(db.HowHeard.Where(x => x.Active == true).OrderBy(x => x.SortOrder), "HowHeardId", "HowHeardType", company.HowHeardId);
  ViewBag.EventCodeId = new SelectList(db.EventCode.Where(x => x.Active == true).OrderBy(x => x.EventCodeDate), "EventCodeId", "EventCodeName", company.EventCodeId);
  return View(company);
}

EDIT 2:

My POST:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> EditMarketing([Bind(Include = "CompanyId,HowHeardId,eNewsletter,EventCodeId,MarketingNotes,RowVersion")] Company company) {
  try {
    if(ModelState.IsValid) {
      TextInfo ti = CultureInfo.CurrentCulture.TextInfo;
      company.Modified = DateTime.UtcNow;
      company.TouchedBy = User.Identity.GetFullNameLF();
      db.Entry(company).State = EntityState.Modified;
      await db.SaveChangesAsync();
      return RedirectToAction("Index", "Company");
    }
  } catch(DbUpdateConcurrencyException ex) {
    var entry = ex.Entries.Single();
    var companyValues = (Company)entry.Entity;
    var databaseValues = (Company)entry.GetDatabaseValues().ToObject();
    if(databaseValues.MarketingNotes != companyValues.MarketingNotes) { ModelState.AddModelError("MarketingNotes", "Current Value: " + databaseValues.MarketingNotes); }
    if(databaseValues.eNewsletter != companyValues.eNewsletter) { ModelState.AddModelError("eNewsletter", "Current Value: " + databaseValues.eNewsletter); }
    if(databaseValues.HowHeardId != companyValues.HowHeardId) { ModelState.AddModelError("HowHeardId", "Current Value: " + db.HowHeard.Find(databaseValues.HowHeardId).HowHeardType); }
    if(databaseValues.EventCodeId != companyValues.EventCodeId) { ModelState.AddModelError("EventCodeId", "Current Value: " + db.EventCode.Find(databaseValues.EventCodeId).EventCodeName); }
    ModelState.AddModelError(string.Empty, "The record you attempted to edit "
      + "was modified by another user after you got the original value. The "
      + "edit operation was canceled and the current values in the database "
      + "have been displayed. If you still want to edit this record, click "
      + "the Save button again. Otherwise click the Back to List hyperlink.");
    company.RowVersion = databaseValues.RowVersion;
  } catch(DataException dex) {
    ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists please inform your Manager, who will inform the developers." + dex);
  }
  ViewBag.HowHeardId = new SelectList(db.HowHeard.Where(x => x.Active == true).OrderBy(x => x.SortOrder), "HowHeardId", "HowHeardType", company.HowHeardId);
  ViewBag.EventCodeId = new SelectList(db.EventCode.Where(x => x.Active == true).OrderBy(x => x.EventCodeDate), "EventCodeId", "EventCodeName", company.EventCodeId);
  return View(company);
}

Please note that I am making use of concurrency to avoid data collisions. Hence the RowVersion column.

René Kåbis
  • 842
  • 2
  • 9
  • 28
  • As always, use view models. You should not be using data models in views when editing. In the POST method, get the original data model, mao the view model properties to it an save the data model. –  Feb 16 '16 at 01:39
  • I have tried that, but the db craps all over my attempt to pull data out of it and into the different view. My original has `Company company = await db.Company.FindAsync(id);` and any attempt to deviate from this by adding the specific model `EditCompanyViewModel company = await db.Company.FindAsync(id);` prevents data from being pulled from the db. – René Kåbis Feb 16 '16 at 01:43
  • Your not understanding what a view model is. You get `Company` as your doing and then initialize a new instance of `CompanyVM` and set its properties from the data model, and return the view model to the view –  Feb 16 '16 at 01:45
  • I have added the example I am fighting with right now. The marketing portion is but a small part of the Company table. Obviously the CompanyId is in there, because changes need to be matched up. – René Kåbis Feb 16 '16 at 01:50
  • You view expects the model to be `EditMarketingViewModel`, but your returning `Company` (so an exception will be thrown). You need `var model = new EditMarketingViewModel { CompanyId = company.CompanyId, HowHeardId = company.HowHeardId, .... }; return View(model);`. And since you do have a view model, then it should also include properties for the SelectList's - e.g. `public IEnumerable HowHeardList { get; set; }` (should not be using `ViewBag` when you have a view model). Also remove the `[Key]` attribute in the view model. –  Feb 16 '16 at 01:56
  • Your suggestions (especially that about the viewbag for selects) does not match up with how ASP.NET scaffolds its own stuff. If you do an automatic scaffold, it uses ViewBag to create selectlists. – René Kåbis Feb 16 '16 at 02:00
  • One of the worst feature MS added :) (but it is scaffolding your data model, not the view model, so its the best they could do I suppose). And no doubt it also created `@Html.DropDownList("HowHeardId", null, ....)` for you which means that you will not even get any validation (refer [this answer](http://stackoverflow.com/questions/35392318/required-attribute-does-not-work-on-foreign-key-dropdown-list/35401529#35401529) for an explanation. –  Feb 16 '16 at 02:05
  • This still doesn't help me. Is there a super-simple online example you can point me toward? Because I am still not understanding you. – René Kåbis Feb 16 '16 at 02:07
  • The answer by John Ephraim Tugado, covers most of it, but I recommend you read the answers to [What is ViewModel in MVC?](http://stackoverflow.com/questions/11064316/what-is-viewmodel-in-mvc) –  Feb 16 '16 at 02:48
  • I can't find "ModelState.IsValid" in his example. Where do I put it? – René Kåbis Feb 16 '16 at 02:50
  • I have edited the answer (note you don't seem to have any validation attributes on your current view model so not sure what your validating?). Also properties such as `Modified` should not be in the view model - the user is not editing it - your set the value in the data model immediately before saving it. –  Feb 16 '16 at 02:56
  • I am using FluentValidation: RuleFor(x => x.PhoneNumber) .NotEmpty().WithMessage("Please enter a valid 10-digit phone number.") .Length(12, 12).WithMessage("Phone number must be in the form of “123-456-7890”") .Matches(@"^\d{3}-\d{3}-\d{4}$").WithMessage("Phone number must be a valid 10-digit phone number with dashes, in the form of “123-456-7890”"); – René Kåbis Feb 16 '16 at 02:57
  • I would also suggest you modify the POST method to check the value of `company.RowVersion` is equal to `viewModel.RowVersion` so that you can immediately add the 'concurrency' error message and return the view, rather that letting an unnecessary exception be thrown. –  Feb 16 '16 at 03:04

1 Answers1

1

As I understand your code EditMarketingViewModel is used to update a company record. Pass your view model as parameter to your post action result. You would want to load the company record first before updating it like so. This approach makes your record retain property values which are not needed to be updated.

  [HttpPost]
  public async Task<ActionResult> Edit(EditMarketingViewModel viewModel)
  {
    if (!ModelState.IsValid)
    {
      ViewBag.HowHeardId = new SelectList(db.HowHeard.Where(x => x.Active == true).OrderBy(x => x.SortOrder), "HowHeardId", "HowHeardType", company.HowHeardId);
      ViewBag.EventCodeId = new SelectList(db.EventCode.Where(x => x.Active == true).OrderBy(x => x.EventCodeDate), "EventCodeId", "EventCodeName", company.EventCodeId);
      return View(viewModel);
    }
    Company company = await db.Company.FindAsync(viewModel.CompanyId);
    if(company == null) {
        return HttpNotFound();
    }
    company.HowHeardId = viewModel.HowHeardId;
    company.eNewsletter = viewModel.eNewsletter;
    // etc.
    // don't need to assign a new value to properties that should be retained
    db.Entry(company).State = EntityState.Modified;
    db.SaveChanges();
    return RedirectToAction("Index");
  }

Update:

You are returning an object of type Company from your GET controller but your view is expecting an EditMarketingViewModel. So do it like this:

// GET: Company/EditMarketing
public async Task<ActionResult> EditMarketing() {
  var id = new Guid(User.GetClaimValue("CWD-Company"));
  Company company = await db.Company.FindAsync(id);
  if(company == null) {
    return HttpNotFound();
  }
  ViewBag.HowHeardId = new SelectList(db.HowHeard.Where(x => x.Active == true).OrderBy(x => x.SortOrder), "HowHeardId", "HowHeardType", company.HowHeardId);
  ViewBag.EventCodeId = new SelectList(db.EventCode.Where(x => x.Active == true).OrderBy(x => x.EventCodeDate), "EventCodeId", "EventCodeName", company.EventCodeId);

  EditMarketingViewModel viewModel = new EditMarketingViewModel()
 {
   CompanyId = company.Id,
   // Other view model properties go here
 } 

  return View(viewModel);
}

Make sure the object type you are returning from the controller to the view matches.

  • Actually, I haven't even gotten to that stage yet. I am still in the "trying to get this into a view in the first place" stage. The code in my OP is at the extract-and-fill-form stage, which is not working. – René Kåbis Feb 16 '16 at 02:15
  • I updated my answer. There is a mismatch on the object data type from your controller to the view – John Ephraim Tugado Feb 16 '16 at 02:21
  • GET seems to work now. Working on POST next. How do I work with BIND in this situation? – René Kåbis Feb 16 '16 at 02:25
  • Good to know it's working. Just make sure you are assigning correct values to your view model during post. – John Ephraim Tugado Feb 16 '16 at 02:31
  • Your POST doesn't seem to be entirely correct, as I never posted my POST and it's considerably different than the GET. How should I "catch" the values, especially implementing BIND? **EDIT:** Added my original POST as a reference. – René Kåbis Feb 16 '16 at 02:32
  • @RenéKåbis, When using a view model, your never use the `[Bind]` attribute. Your view model only contains the properties you need. And in any case you mapping the view model properties your want from the view model to the data model (your fully protected from over-posting attacks). –  Feb 16 '16 at 02:35
  • So how would I do modelstate validation if what is being brought back isn't the model that is expected? It isn't Company per se, it is a custom model. I have extensive FluentValidation rules that I wish to employ against the model being posted. – René Kåbis Feb 16 '16 at 02:40
  • Plus, I have concurrency checks via the RowVersion column. Please see Edit2 of my original post. – René Kåbis Feb 16 '16 at 02:46
  • Stephen updated the answer and added the viewmodel validation on post which is what you need. – John Ephraim Tugado Feb 16 '16 at 07:21