Zhi Kai
Zhi Kai

Reputation: 1587

ASP.NET MVC5 Controller CRUD with ViewModel (2 models)

In my controller, I have a ViewModel containing 2 different models, called Drug and Food.

public class FoodDrugViewModel {
        public IEnumerable<SGHealthDesktop.Models.Drug> Drugs { get; set; }
        public IEnumerable<SGHealthDesktop.Models.Food> Foods { get; set; }

    }

In my MainController, this is how I pass in the ViewModel into the Index.

 // GET: Admin
        public ActionResult Index() {
            FoodDrugViewModel vm = new FoodDrugViewModel(); //initialize it
            vm.Drugs = db.Drugs.ToList();
            vm.Foods = db.Foods.ToList();
            return View(vm);
        }

In my view, I created two tables, and looped the items in each of the model, like this.

<table class="table" id="drugTable">
    <thead>
        <tr>
            <th>Drug Name</th>
            <th>Dosage</th>
            <th>Unit</th>
            <th>Type</th>
        </tr>
    </thead>
    @foreach (var item in Model.Drugs) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.DrugName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Dosage)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Unit)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Type)
            </td>
            <td>
                @Html.ActionLink("Edit", "Edit", new { id = item.DrugId }) |
                @Html.ActionLink("Details", "Details", new { id = item.DrugId }) |
                @Html.ActionLink("Delete", "Delete", new { id = item.DrugId })
            </td>
        </tr>
    }
</table>

 @foreach (var item in Model.Foods) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.FoodName)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Protein)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Carbohydrate)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.TotalFat)
        </td>
        <td>
            @Html.ActionLink("Edit", "Edit", new { id = item.FoodId }) |
            @Html.ActionLink("Details", "Details", new { id = item.FoodId }) |
            @Html.ActionLink("Delete", "Delete", new { id = item.FoodId })
        </td>
    </tr>
}

To prevent both tables from showing up at the same time, I used a dropdownlist accompanied with JQuery so user can choose which table to see, and it's working as expected. However, my issue are as follow.

When I click on the "Details" ActionLink, or infact any of the 3 ActionLinks (Detail, Edit, Delete), I would like the relevant information to be displayed. For example, if I'm viewing the Drug table and if I click on "Details", the Detail view will display the drug information, and the same for Food.

However, I can't seem to figure out how that can be achieved. My Detail method is as follow, having Drug as the main model still. How can it detect if the user has chose to view the detail of Drug OR Food? As you can see in the code, it immediately finds the Drug details based on the id.

// GET: Admin/Details/5
        public ActionResult Details(int? id) {
            if (id == null) {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            Drug drug = db.Drugs.Find(id);
            if (drug == null) {
                return HttpNotFound();
            }
            return View(drug);
        }

As for Create, there's no problem with it as I can again, allow a dropdownlist so user can choose what type, Drug or Food, they want to create and the form will be shown respectively (assuming in view I'm using the FoodDrugViewModel instead of Drug model). But how can i bind the data in the controller? By default, the Create method is as follow.

[HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create([Bind(Include = "DrugId,DrugName,Dosage,Unit,Type")] Drug drug) {
            if (ModelState.IsValid) {
                db.Drugs.Add(drug);
                db.SaveChanges();
                return RedirectToAction("Index");
            }

            return View(drug);
        }

Any help rendered will be much appreciated. Thanks in advance!

UPDATE: Issue on Create()

In the Create view, I declared the FoodDrugViewModel as follow

@model SGHealthDesktop.ViewModels.FoodDrugViewModel

And my Drug form looks like this (the same applies for Food).

<div id="drugDiv">
            <div class="form-group">
                @Html.LabelFor(model => model.Drug.DrugName, htmlAttributes: new { @class = "control-label col-md-2" })
                <div class="col-md-10">
                    @Html.EditorFor(model => model.Drug.DrugName, new { htmlAttributes = new { @class = "form-control" } })
                    @Html.ValidationMessageFor(model => model.Drug.DrugName, "", new { @class = "text-danger" })
                </div>
            </div>

            <div class="form-group">
                @Html.LabelFor(model => model.Drug.Dosage, htmlAttributes: new { @class = "control-label col-md-2" })
                <div class="col-md-10">
                    @Html.EditorFor(model => model.Drug.Dosage, new { htmlAttributes = new { @class = "form-control" } })
                    @Html.ValidationMessageFor(model => model.Drug.Dosage, "", new { @class = "text-danger" })
                </div>
            </div>

            <div class="form-group">
                @Html.LabelFor(model => model.Drug.Unit, htmlAttributes: new { @class = "control-label col-md-2" })
                <div class="col-md-10">
                    @Html.EditorFor(model => model.Drug.Unit, new { htmlAttributes = new { @class = "form-control" } })
                    @Html.ValidationMessageFor(model => model.Drug.Unit, "", new { @class = "text-danger" })
                </div>
            </div>

            <div class="form-group">
                @Html.LabelFor(model => model.Drug.Type, htmlAttributes: new { @class = "control-label col-md-2" })
                <div class="col-md-10">
                    @Html.DropDownListFor(model => model.Drug.Type,
                     new SelectList(new[]
                    { "Diabetic Medication", "Hypertension", "Kidney Disease", "Insulin", "High Cholesterol"
                    }) as SelectList, new { @class = "btn btn-default dropdown-toggle form-control" })

                    @Html.ValidationMessageFor(model => model.Drug.Type, "", new { @class = "text-danger" })
                </div>
            </div>
        </div>

My Create() method is as follow

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create([Bind(Include = "DrugName,Dosage,Unit,Type")] FoodDrugViewModel vm) {
            try {
                if (ModelState.IsValid) {
                    if (vm.Drug != null) {
                        db.Drugs.Add(vm.Drug);
                    } 
                    db.SaveChanges();
                    return RedirectToAction("Index");
                }
            } catch (DataException dex) {
                //Log the error (uncomment dex variable name and add a line here to write a log.
                System.Diagnostics.Debug.WriteLine(dex);
                ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
            }
            return View(vm.Drug);
        }

I placed a breakpoint at the line the method is called, and "Drug" is null. May I know where did I went wrong? :(

Upvotes: 0

Views: 1050

Answers (2)

Nifal Nizar
Nifal Nizar

Reputation: 963

I can suggest you two ways to do this.

  1. For both Drugs and Foods create 6 different actions like

Food_Edit, Food_Details, Food_Delete, Drug_Edit, Drug_Details, Drug_Delete

  1. Pass Another parameter for the actions indicating the type. In that case you may have to right another routing method.

@Html.ActionLink("Edit", "Edit", new { id = item.FoodId, type="Food" })

@Html.ActionLink("Edit", "Edit", new { id = item.DrugId, type="Drug" })

Upvotes: 1

Guruprasad J Rao
Guruprasad J Rao

Reputation: 29683

You can pass one more parameter to your ActionResult to differentiate between Drug and Food. For an example, I would be adding type param, with value drug and food for respective actions.

Drug


@foreach (var item in Model.Drugs) {
<tr>
  <td>
    @Html.DisplayFor(modelItem => item.DrugName)
  </td>
  <td>
    @Html.DisplayFor(modelItem => item.Dosage)
  </td>
  <td>
    @Html.DisplayFor(modelItem => item.Unit)
  </td>
  <td>
    @Html.DisplayFor(modelItem => item.Type)
  </td>
  <td>
    @Html.ActionLink("Edit", "Edit", new { id = item.DrugId, type="drug" }) | @Html.ActionLink("Details", "Details", new { id = item.DrugId, type="drug" }) | @Html.ActionLink("Delete", "Delete", new { id = item.DrugId, type="drug" })
  </td>
</tr>
} 

Food


@foreach (var item in Model.Foods) {
<tr>
  <td>
    @Html.DisplayFor(modelItem => item.FoodName)
  </td>
  <td>
    @Html.DisplayFor(modelItem => item.Protein)
  </td>
  <td>
    @Html.DisplayFor(modelItem => item.Carbohydrate)
  </td>
  <td>
    @Html.DisplayFor(modelItem => item.TotalFat)
  </td>
  <td>
    @Html.ActionLink("Edit", "Edit", new { id = item.FoodId, type="food" }) | @Html.ActionLink("Details", "Details", new { id = item.FoodId, type="food" }) | @Html.ActionLink("Delete", "Delete", new { id = item.FoodId, type="food" })
  </td>
</tr>
}

Your ActionResult Details now should be accepting two arguments, id and type.

// GET: Admin/Details/5
public ActionResult Details(int? id, string type) {
  //You do not want to do anything if you don't have type value too, so the condition
  if (id == null || string.IsNullOrEmpty(type)) {
    return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
  }
  if(type=="drug"){
      Drug drug = db.Drugs.Find(id);
      if (drug == null) {
         return HttpNotFound();
      }
      return View(drug);
  }
  else
  {
      Food food = db.Foods.Find(id);
      if (food == null) {
         return HttpNotFound();
      }
      return View(food);
  }
}

Hope you will be handling your view efficiently with different models passed.


EDIT

You might also check it in below way, by adding ternary operation, but am not sure whether it will work or not. You can give a try.

// GET: Admin/Details/5
public ActionResult Details(int? id, string type) {
  //You do not want to do anything if you don't have type value too, so the condition
  if (id == null || string.IsNullOrEmpty(type)) {
    return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
  }
  var model=type=="drug"?db.Drugs.Find(id):db.Foods.Find('id');
  if (model == null) {
      return HttpNotFound();
  }
  return View(model);
}

Upvotes: 2

Related Questions