0

I am learning asp.net mvc , using visual studio community 2017, and as a sort of teaching project I am making a web app that keeps track of exercise work outs. My model consists of WorkOut objects that have a list (or ICollection more specifically) of Exercise, and each Exercise has an ICollection. Heres the basics of my model classes.

public class WorkOut
    {
        public int Id { get; set; }
        public int Length { get; set; }
        public DateTime Date { get; set; }
        public virtual ICollection<Exercise> ExerciseList { get; set; }
    }
public class Exercise
    {

        public int Id { get; set; }
        public string Name { get; set; }
        public int WorkOutId { get; set; }
        public virtual WorkOut WorkOut { get; set; }
        public virtual ICollection<RepUnit> Sets { get; set; }
    }
public class RepUnit
    {
        public int Id { get; set; }
        public int Rep { get; set; }
        public int? Weight { get; set; }
        public int ExerciseId { get; set; }
        public virtual Exercise Exercise { get; set; }
    }

Generating a view automatically with WorkOut as a model leads to Create action and view that only generates a Length and Date property. In general, auto generated view and controllers only add the non virtual properties. So I figure maybe I have to do a multistep creation process; Create a workout, create an exercise and add reps to it, add that exercise to the work out, either stop or add another exercise. So I figured Id let VS to some of the work for me, and I make controllers and views for each of the model object typers (WorkOutsController, ExercisesController, RepUnitsController), and later I would trim out the uneeded views or even refactor the actions i actually use into a new controller. So WorkOutsController my POST action is this.

public ActionResult Create([Bind(Include = "Id,Length,Date")] WorkOut workOut)
        {
            if (ModelState.IsValid)
            {
                db.WorkOuts.Add(workOut);
                db.SaveChanges();

                return RedirectToAction("Create","Exercises",new { workoutId = workOut.Id });
            }
            return View(workOut);
        }

So I carry the workoutId to the Exercise controller but this is where I am unsure how to proceed. I want to keep carrying around the workoutId and for the next step, where I give the exercise a name, also show the associated date that was just added. The only thing I could think to do was instantiate an Exercise in the GET action of ExerciseController like so.

public ActionResult Create(int workoutID)
        {
            Exercise ex= new Exercise();
            ex.WorkOutId=workoutID;
            ex.WorkOut=db.WorkOuts(workoutID);

            return View(ex);
        }

This seems terrible and I've not seen anything like this done in any examples, but it seems to work. The same exercise object is brought back to my POST create action here

public ActionResult Create([Bind(Include = "Id,Name,WorkOutId")] Exercise exercise)
        {
            if (ModelState.IsValid)
            {
                db.Exercises.Add(exercise);
                db.SaveChanges();
                return RedirectToAction("Create", "RepUnits", new { exerciseId = exercise.Id });
            }
            return View(exercise);
        }

which as you see calls the RepUnits controller and associated Create action. There I do something very similar; create a rep object and pass it to the view, and essentially I add reps until I'm done. Eventually I will create navigation to either go back to add a new exercise or go back to an Index view.

So to sum up, it seems wasteful to be passing entire objects around, and maybe my whole implementation is wrong and I should be trying to somehow do this all on one form. Up to this point googling hasnt found me much because I wasnt sure what questions to be asking, however this post Creation of objects using form data within an ASP.NET MVC application just popped up in the similar question dialogue and the app in question is coincidentally very similar! However when the OP mentions passing the workoutId around, how is this accomplised? I thought to maybe use the ViewBag but how do I get the view to handle this Id? I had though to try, as an example

 public ActionResult Create(int workoutId)
        {
            ViewBag.WoID = workoutId;
            return View();
        }

in the ExercisesController and then in the associated Create view:

 @Html.Hidden("WorkOutId", new { ViewBag.WoID })

But later in the view when I try to reference the workout date it comes up blank

<div class="form-group">
            @Html.LabelFor(model => model.WorkOut.Date, "Work Out On:", htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.DisplayFor(model=>model.WorkOut.Date)
            </div>
        </div>

Should I be doing something like this in the view: @Model.WorkOutId=ViewBag.WoID;

which doesnt work for some reason (Compiler Error Message: CS1525: Invalid expression term '='), but is that along the lines of how I pass these ids around?

Community
  • 1
  • 1
greedyLump
  • 220
  • 3
  • 15
  • Try `@{ Model.Foo = ViewBag.Bar; }` as you need the `@` to denote a code block rather than *print-here* operator. – Wiktor Zychla May 04 '17 at 16:31
  • When I try that inside my form, @using (Html.BeginForm()) { @Html.AntiForgeryToken() @Html.Hidden("WorkOutId", new { ViewBag.WoID }) @{ Model.WorkOutId = ViewBag.WoID; } I get Parser Error Message: Unexpected "{" after "@" character. Outisde the form I get a null exception error – greedyLump May 04 '17 at 17:26

1 Answers1

0

The scaffolded views are intentionally simplistic. Dealing with related items requires multiple considerations, and Visual Studio won't make those for you. However, you can and are very encouraged to alter the scaffolded views to your particular needs.

To create exercises in the same view as your workout for example, you need only generate fields for Exercise with names that will allow the modelbinder to bind the posted data. For collection properties that means something like CollectionProperty[N].Property, where N is an index.

For example, you can initialize your workout with three exercises:

var model = new Workout
{
    ExerciseList = new List<Exercise>
    {
        new Exercise(),
        new Exercise(),
        new Exercise()
    }
};

Then, in your view:

@for (var i = 0; i < Model.Exercises.Count(); i++)
{
    @Html.EditorFor(m => m.ExerciseList[i].Name)
}

However, there's one thing to note here: ICollection is not indexable. As a result, to do it this way, you'd need a property typed as List<Exercise>. This is where view models come in very handy. Nevertheless, there is a way around this: you can use EditorFor on the Excercises collection instead. For example, the above code would be reduced to just:

@Html.EditorFor(m => m.ExerciseList)

EditorFor is a "templated helper", which means simply that it uses templates to render what's passed to it. Thankfully, it has some defaults, so to a point, you don't need to worry about that, but it become problematic. For example, here, Razor will simply iterate over the items in ExerciseList and render the template for Exercise for each. Since Exercise is a custom type and doesn't have a default template, it will then introspect the class and render a template for each public property on Exercise. Sometimes this works just fine. For example, Name will be rendered with a text box, as it should be. However, you'll also get text boxes for Id and WorkoutId, which you wouldn't want to even be visible on your form.

You can solve this issue by creating your own editor template for Exercise, by adding a view to Views\Shared\EditorTemplates\Exercise.cshtml. This view would have a model of Exercise, then, and would simply include a text box for your name property. Then, when you use EditorFor on ExerciseList as above, it will render each Exercise utilizing that view.

With all that out of the way, though, you've likely realized that this is still somewhat limiting: you have to initialize with a certain number of exercises and then that's all you get. That's where JavaScript comes in. Instead of iterating over a predefined list of exercises, you can simply dynamically add a new block of exercise fields as needed (or remove existing blocks). However, writing JavaScript for this task manually would be very painstaking and dense. At this point, you're better off utilizing something like Knockout.js, Angular, or similar. These libraries, among other things, give you two-way databinding, so you could simply set up a JavaScript template for what a block of exercise fields will look like, and then bind that to an ExceriseList member of a JavaScript object (your client-side "view model"). You could then cause these fields to repeat simply by adding or removing items from this JS array. Obviously, there's much more that goes into this, but that's the basic framework. You'd need to consult the individual documentation of the library you went with to determine exactly how to set everything up.

You can then rinse and repeat all this for other levels of relationships as well. In other words, it's entirely possible to post this entire object graph of a workout with multiple exercises, each with multiple rep units, etc. all in one go with one view.

Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • TY for your attention to this. Indeed, starting with a predefined number of exercises will not work. But maybe this tells me the direction i need to go. I've put off getting too deep into JS but I guess its time. – greedyLump May 04 '17 at 17:32