1

I've looked, tried several different solutions and haven't found anything that works (at least, not something with an example close enough to what I want for me to follow). I'm sure I'm missing something that would be a simple thing to a more experienced coder. Help?

I have a Model called Residents. It includes ResidentID, PFName, PLName. I have a controller for Residents. I have CRUD views for Residents. All working just fine.

I have a Model called Logs. It includes LogID, ResidentID, Comments. I have a controller for Logs. I have CRUD views for Logs. All working just fine.

I can display all the log entries for a Resident. Works fine. After a Log entry has been created, I can display the PFName using the method

@Html.DisplayFor(model => model.Resident.PFName) 

Next, I want to Create a new log entry for a selected Resident.

That's where I'm having the problem. I would like the "Create" view (for the Log) to display the ResidentFName and ResidentLName of the selected resident, not the ResidentID.

A this point, from the Details view for a Resident, I have a CreateLog link.

@Html.ActionLink("New Log Entry", "../Log/Create", new { @ResidentID = Model.ResidentID})

This (likely not the best way) gives me a URL with the value of the selected ID

http://localhost:999/Log/Create?ResidentID=1

The value for the ResidentID is correct; it changes depending on which Resident is selected.

This value is correctly entered

        @Html.TextBoxFor(model => model.ResidentID)

on the new CreateLog page using the Log Controller Create action.

public ActionResult Create(int ResidentID)

I plan to hide the ResidentID TextBox so the user doesn't see it. It seems I have to make it available in the form to be able create a new log entry.

The CreateLog form currently works as I have it now. I can create a log entry and verify that entry has been correctly recorded for the Resident.

But, I would like the form to display the PFName and PLName for the Resident so the user has visible feedback for which Resident was selected.

I believe that the related data (PFName and PLName) I want has to be passed to the CreateLog form .... somehow. I can't get it from the form.

Since there's only the unsaved entry for ResidentID, I can't use the value from the CreateLog form it to display related data. As mentioned, for the Lists, there is no such problem. It's only for CreateLog.

I've tried adding the data to the URL. Not working. I've tried setting the strings in the Controller (and the URL). Not working. I've looked at setting a cookie, but haven't ever done that so not sure what to set or where to put it or how to get the values from it. I've looked at setting a variable in the controller ... (have that working to display drop down lists, but a list to select from is not what I need -- I want the matching values from the related table).

Log.LogID(PK, Identity)
Log.ResidentID(FK)
Resident.PFName
Resident.PLName

I can directly create a view with these tables/fields in my SQLDB and update it.

Terri
  • 354
  • 6
  • 18

4 Answers4

2

Assuming a view model which looks something like this:

public class CreateLogViewModel
{
    public int ResidentID { get; set; }
    public string PFName { get; set; }
    public string PLName { get; set; }

    public string SomeLogCreationProperty { get; set; }
    // other properties
}

Your controller could look something like this:

public ActionResult Create(int ResidentID)
{
    var model = db.Residents.Where(r => r.ResidentID == ResidentID)
                .Select(r => new CreateLogViewModel
                {
                    ResidentID = r.ResidentID,
                    PFName = r.PFName,
                    PLName = r.PLName
                    // other properties
                });

    return View(model);
}

Then the view:

@model CreateLogViewModel

@using (Html.BeginForm())
{
    @Html.HiddenFor(m => m.ResidentID)
    @Html.HiddenFor(m => m.PFName)
    @Html.HiddenFor(m => m.PLName)

    @Html.EditorFor(m => m.SomeLogCreationProperty)
    // other properties
    <input type="submit" />
}

This would then POST back to:

[HttpPost]
public ActionResult Create(CreateLogViewModel model)
{
    if (ModelState.IsValid)
    {
        return RedirectToAction("Index");
    }

    // Redisplay the form with errors
    return View(model);
}
John H
  • 14,422
  • 4
  • 41
  • 74
  • 1
    Thank you! I will have to test it, but it looks like the "SomeLogCreationProperty" you included could be the missing piece. – Terri Jan 13 '14 at 03:34
  • Unfortunately, not working. I created a new class with all the needed fields. I changed the controller "Create" to include the db call and now I'm getting an error. 'The model item passed into the dictionary is of type 'System.Data.Entity.Infrastructure.DbQuery`1[DSet.Models.CreateLog]', but this dictionary requires a model item of type 'DSet.Models.CreateLog'.' I understand the error -- it says I am doing exactly what I did. Not quite sure how to fix it, though. – Terri Jan 15 '14 at 01:09
  • @Terri You have to make sure you're projecting onto the `CreateLog` type. That is: `Select(x => new CreateLog { ... })`. However, `CreateLog` should also not be an entity type. A view model is a separate thing entirely and is designed specifically to represent a slice of your data. That is, the view model represents all of the data needed for the view to be displayed. – John H Jan 15 '14 at 01:20
1

Expanding on John H and StuartLC answers, you need to use ViewModels and the following workflow:

Database->(load)->Model->Controller->(convert)->ViewModel->View

and

View->ViewModel->Controller->(convert)->Model->(save)->Database

So lets says you have the following models:

namespace Models
{
    public class Residents
    {
        public int ResidentID { get; set; }
        public string PFName { get; set; }
        public string PLName { get; set; }
        //...
    }

    public class Logs
    {
        public int LogID { get; set; }
        public int ResidentID { get; set; }
        public string Comments { get; set; }
        //...
    }

}

You need a ViewModel that combines the data you need for display and input in your Log\CreateView:

namespace ViewModels
{
    public class ResidentLog
    {
        public int ResidentID { get; set; }
        public string PFName { get; set; }
        public string PLName { get; set; }
        public string Comments { get; set; }
        //...
    }
}

Then inside the controller:

public class LogController : Controller
{
    [HttpGet]
    public ActionResult Create(int ResidentID)
    {
        // Run in debug and make sure the residentID is the right one
        // and the resident exists in the database
        var resident = database.Residents.Find(residentID);

        var model = new ViewModels.ResidentLog
        {
            ResidentID = resident.ResidentID,
            PFName = resident.PFName,
            PLName = resident.PLName,
            Comments = string.Empty,
            // ...
        };

        // Run in debug and make sure model is not null and of type ResidentLog
        // and has the PFName and PLName
        return View(model);
    }

    [HttpPost]
    public ActionResult Create(ViewModels.ResidentLog model)
    {
        if (!ModelState.IsValid)
            return View(model);

        var log = new Models.Logs 
        { 
           // Assumes LogID gets assigned by database?
           ResidentID = model.ResidentID,
           Comments = model.Comments,
        };

        // Run in debug and make sure log has all required fields to save
        database.Logs.Add(log);
        database.SaveChanges();

        return RedirectToAction("Index"); // Or anywhere you want to redirect
    }
}

Then your Log\CreateView:

@model ViewModels.ResidentLog

<!-- Display the values needed -->
<div>@Model.ResidentID - @Model.PFName - @Model.PLName</div>

@using (var form = Html.BeginForm(...))
{
    <!-- This saves the values for the post, but in fact only ResidentID is actually used in the controller -->
    @Html.HiddenFor(m => m.ResidentID)
    @Html.HiddenFor(m => m.PFName)
    @Html.HiddenFor(m => m.PLName)

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

    <input type="submit" />
}
Karhgath
  • 1,809
  • 15
  • 11
  • The idea that I need to return View(something) makes sense, but the variable set for the controller doesn't work. There's a "doesn't exist" error for r.' var model = new ViewModels.ResidentLog { ResidentID = r.ResidentID, PFName = r.PFName, PLName = r.PLName, Comments = string.Empty, // ... }; – Terri Jan 18 '14 at 15:39
  • Sorry, 'r' should be 'resident', fixed the typo above. – Karhgath Jan 18 '14 at 15:49
  • Working!!! Yippee! So the just posted "doesn't exist" problem was a naming convention issue. Fixed that. So in the controller 'code' var resident = db.Residents.Find(ResidentID); var nl = new NewLog { ResidentID = ResidentID, PFName = resident.PFName, PLName = resident.PLName, Comment = string.Empty, }; return View(nl);'code' – Terri Jan 18 '14 at 16:01
0

You need to provide the additional information to the view. This can be done in at least 2 ways

  1. Use the ViewBag dynamic as a quick and dirty cheap and cheerful container to pass everything the view needs from the controller.
  2. (preferred) Use a custom ViewModel with a tailor made class which holds everything the view needs. This is generally preferred as it is statically typed.

(I'm assuming that resident is already persisted in the database by the time the Log controller is called - you might need to fetch it elsewhere)

So, in your log controller, here's an example of using ViewBag:

[HttpGet]
public ActionResult Create(int residentID)
{
    ViewBag.Resident = Db.Residents.Find(residentId);
    return View();
}

You can then show the resident properties on the view by utilizing the ViewBag.

Edit

Yes, by persisted I meant in the Db - apologies about using unclear jargon.

Here's another example of ViewBag approach (the idea is to create a new Comment for another object):

Doing this the cheap + cheesy ViewModel way - in the HTTPGet Controller Create method:

    public ActionResult Create(string objectType, int objectId)
    {
        // This is equivalent to youn fetching your resident and storing in ViewBag
        ViewModel.Object = FetchSomeObject(objectType, objectId);
        return View();
    }

And in the View I use this (The ViewBag is accessible to Controller and View):

<title>@string.Format("Add new Comment for {0} {1}", ViewBag.Object.ObjectType, ViewBag.Object.Name);</title>

As you say, you will also need to do add a hidden for the ResidentId in your create log form

As per @JohnH's answer (+1), the BETTER way to do this (than using the magic ViewBag dynamic) is to create a custom ViewModel specifically for this screen. The ViewModel can either be reused both ways (GET: Controller => View and POST : Browser => Controller, or you even have separate ViewModels for the Get and Post legs.

Community
  • 1
  • 1
StuartLC
  • 104,537
  • 17
  • 209
  • 285
  • @StuartLC... You clarified a "missing" piece (existing). But not the solution. CreateLog() uses db.Log (and the Log Controller (as it should) to create the new Log entry, not db.Residents. I'm passing the ResidentID for the yet-to-be-created Log record via URL+Log Controller -- so at the time I want to get related Names, the ID does not exist for the record; it's only visible in the View. One of the solutions I tried was creating a combo Model/class to include both the Log and Resident models. I got several errors in other Views and reverted to the URL method (not quite right but no errors). – Terri Jan 13 '14 at 03:50
  • I'm not quite following you. If the resident entity is available (e.g. persisted) at the time of generating the `LogCreate` view from the `LogController` then you can populate the resident in the `ViewBag` or preferably a custom `ViewModel` for the view. If the resident is created on the same screen at the same time, then a browser side solution is needed, e.g. with javascript / jquery. – StuartLC Jan 13 '14 at 05:15
  • Seems terminology is clouding the problem. At the time I am creating the log entry, a record for the Resident (including ID, PFName, PLName) is stored in the database (whether this equates to "persisted" is not clear to me). There is no Resident information stored ("?persisted?) in the form. The form is empty. I am using the URL and 'public ActionResult Create(int ResidentID)' in the controller to "enter text" (the ResidentID) in the New form. I tried adding 'ViewBag.Resident = db.Residents.Find(ResidentID);' to the action as you suggested. There was no change. Neither Name displays. – Terri Jan 15 '14 at 01:26
0

With much thanks to all, I have it working. The final piece was telling the controller to return the model (nl). Here's the full spec for what's working:

I have created a ViewModel that includes

public class NewLog
{
    public int ResidentID { get; set; }
    public string PFName { get; set; }
    public string PLName { get; set; }
    public string Comment { get; set; }
    // other properties
}

In the LogController,

public ActionResult Create(int ResidentID)
{
    var resident = db.Residents.Find(ResidentID);

    var nl = new NewLog
    {
        ResidentID = ResidentID,
        PFName = resident.PFName,
        PLName = resident.PLName,
        Comment = string.Empty,
    };
    return View(nl);
}

In the Create.cshtml page,

@model My.Models.NewLog

The required ResidentID to be recorded with the new Log Entry

        @Html.TextBoxFor(model => model.ResidentID, new {@Type = "Hidden"})

And the related, user-friendly display boxes for the person's name

        @Html.DisplayFor(model => model.PFName)
        @Html.DisplayFor(model => model.PLName)

And in the URL which is used to access the create page,

    @Html.ActionLink("New Log Entry", "../Log/Create", new { @ResidentID = item.ResidentID, item.PFName, item.PLName})
Terri
  • 354
  • 6
  • 18