0

I'm rewriting this question:

I have 2 models. Entry and Topic.

public class Entry
{
    public int EntryId { get; set; }
    public int UserId { get; set; }
    public int TopicId { get; set; }

    public String EntryQuestion { get; set; }
    public String EntryAnswer { get; set; }
    public int EntryReview { get; set; }
    public String QuestionValidationURL { get; set; }

    public virtual ICollection<Topic> TopicList { get; set; }
}

public class Topic
{
    public int TopicId { get; set; }
    public String TopicName { get; set; }
}

I followed an example on ASP.Net/MVC to set up my models this way. What I would like to do is for every entry item I have a TopicId, but then I'd like to convert that to a TopicName by accessing my TopicList.

My question is, how do I load TopicList?

In the examples I'm following I'm seeing something about LazyLoading and EagerLoading, but it doesn't seem to be working.

I tried doing the following from my Entry controller:

    db.Entries.Include(x => x.TopicList).Load();

But that still gives me a TopicList of 0 (which is better than null)

How can I do this?

In my view I'm binding to the Entries like this:

@model IEnumerable<projectInterview.Models.Entry>

I would like to access the TopicList here:

@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.TopicId)
        </td>
        ...
    </tr>

I'd like to use the TopicId in this loop and display the TopicName that is part of the object in the collection.

webdad3
  • 8,893
  • 30
  • 121
  • 223
  • Please show how do you bind the Model and use it on a view. – Dima Aug 06 '13 at 03:51
  • please show the `getTopicsCount()` method – bazz Aug 06 '13 at 04:40
  • Are you acutally instantiating the _collection_ at any point? Either you need to do so when you create the model by hand or when it's done for you by the framework (when used as a parameter to an action)... – Basic Aug 06 '13 at 06:10

5 Answers5

3

I'm assuming you're following an Entity Framework example. You're trying to create a one-to-many relationship, as far as I can tell, although I'm unsure about which end is which.

In the general case, to establish a one-to-many relationship, you have to do something like this:

public class One
{
    [Key]
    public int Id { get; set; }

    public virtual ICollection<Many> Many { get; set; }
}

public class Many
{
    [Key]
    public int Id { get; set; }

    [ForeignKey("One")]
    public int OneId { get; set; }
    public virtual One One { get; set; }
}

If what you're trying to do is have one Entry relating to many Topic objects, then you're almost there but you're lacking something.

For the ICollection<Topic> to actually contain anything, the (many) Topic objects need to have a foreign key to the (one) Entry. (It also doesn't hurt to explicitly mark the primary key on both sides, rather than relying on the EF conventions.)

public class Topic
{
    [Key]
    public int TopicId { get; set; }
    public String TopicName { get; set; }

    [ForeignKey("Entry")]
    public int EntryId { get; set; }
    public virtual Entry Entry { get; set; }
}

public class Entry
{
    [Key]
    public int EntryId { get; set; }

    public int UserId { get; set; }
    public int TopicId { get; set; }

    public String EntryQuestion { get; set; }
    public String EntryAnswer { get; set; }
    public int EntryReview { get; set; }
    public String QuestionValidationURL { get; set; }

    public virtual ICollection<Topic> TopicList { get; set; }
}

Now TopicList should be an actual and populated collection, without the need to do an Include.


If, on the other hand, you want one Topic relating to many Entry objects, then you have it a little backwards. The correct way would be:

public class Topic
{
    [Key]
    public int TopicId { get; set; }

    public String TopicName { get; set; }

    public virtual ICollection <Entry> Entries { get; set; }
}

public class Entry
{
    [Key]
    public int EntryId { get; set; }

    public int UserId { get; set; }

    public String EntryQuestion { get; set; }
    public String EntryAnswer { get; set; }
    public int EntryReview { get; set; }
    public String QuestionValidationURL { get; set; }

    [ForeignKey("Topic")]
    public int TopicId { get; set; }
    public virtual Topic Topic { get; set; }
}

In this case, you may or may not use db.Entries.Include(x => x.Topic) depending on whether you want them loaded all at once or one-by-one on demand. Regardless of what you choose, the following expression should return the proper value:

myEntry.Topic.TopicName
Theodoros Chatzigiannakis
  • 28,773
  • 8
  • 68
  • 104
  • @webdad3 Which side do you intend to be the one side and which the many side? I've included examples for both, but if the answer is too long I could delete the wrong one. – Theodoros Chatzigiannakis Aug 08 '13 at 14:08
  • An entry will only ever have 1 topic. The Topic object is my lookup table (ID,Name). I pass the TopicId into an Entry object. However, what I was thinking was that the Entry.TopicList will be my lookup list so I can convert the TopicId to the TopicName. Maybe that needs to be a dictionary (Key,ValuePair)... Did I just make this a lot more complicated? – webdad3 Aug 08 '13 at 14:17
  • @webdad3 I think I'm getting what you had in mind, but you don't have to do this, because the dynamic proxy classes of the EF will take care of that for you. In short, if you have a property with the `ForeignKey` attribute set properly, then the (virtual) navigation property will fetch the correct entity automatically (by doing the lookup for you behind the scenes). – Theodoros Chatzigiannakis Aug 08 '13 at 14:28
  • Ok I figured that there was an easier way of doing this. Do you have an example of how I can do what I'm trying to describe? – webdad3 Aug 08 '13 at 14:32
  • @webdad3 This is exactly what the foreign key attributes and the virtual navigation properties are there to do. So simply follow the last portion of my answer and you should get it working. The final expression that will return you the `TopicName` (once you've set the rest up) is shown at the very end of the answer. Try it and see if you have any trouble. – Theodoros Chatzigiannakis Aug 08 '13 at 14:33
1

If I understand you correctly you have added the list of Topics to the Entry just to get the name of the topic when displaying the entry. The best way to do this is to actually have a Topic property in your entry model. So your model would look like this:

public class Entry
{
    public int EntryId { get; set; }
    public int UserId { get; set; }
    public int TopicId { get; set; }
    public String EntryQuestion { get; set; }
    public String EntryAnswer { get; set; }
    public int EntryReview { get; set; }
    public String QuestionValidationURL { get; set; }
    //Change this.....
    public virtual Topic Topic { get; set; }
}

Then in your view you would use (assuming the Model is an IEnumerable):

@foreach (var item in Model) {
<tr>
    <td>
        @Html.DisplayFor(modelItem => modelItem.Topic.TopicName )
    </td>
    ...
</tr>

This link has a great example of how to do this: http://weblogs.asp.net/manavi/archive/2011/03/28/associations-in-ef-4-1-code-first-part-2-complex-types.aspx

JTMon
  • 3,189
  • 22
  • 24
  • This worked as you said it would. Can you tell me what I was doing wrong and why this worked? – webdad3 Aug 09 '13 at 01:34
  • When you have an object as a property in another object, EF will do the mapping for you automatically. While if it is a collection, even if you link it and map it properly, EF will fill it for you, but will not "guess" your intention to link topicId with one of the topics in the list. So you would have to have that logic built into your model somewhere. When the property is an object, EF will automatically fill all of its properties, hence you get the Name property filled in automatically. I hope this clarifies things and not make them murkier :) – JTMon Aug 09 '13 at 10:52
0

In my opinion problem is with casting. In view you have IEnumerable<projectInterview.Models.Entry> while Topics is ICollection<Topic>, which is a collection of different type

Piotr Stapp
  • 19,392
  • 11
  • 68
  • 116
0

Topics = null means there are no Topics in the list to iterate over. How do you fill them? Your view expects IEnumerable how do you cast your topics to the entries?

Based on the original question I've added a small working example, maybe it helps you to find your bug.

Controller:

public class TestController : Controller
{
    public ActionResult Index()
    {
        var viewModel = new ViewModel()
        {
            Topics = new List<Topic>()
        };

        viewModel.Topics.Add(new Topic() { header = "test" });
        viewModel.Topics.Add(new Topic() { header = "test2" });

        return View(viewModel);
    }

}

Model:

public class ViewModel
{
    public virtual ICollection<Topic> Topics { get; set; }

    public int getCount()
    {
        return Topics.Count;
    }
}

public class Topic
{
    public string header { get; set; }
}

View:

@model testProject.Models.ViewModel
@{
   ViewBag.Title = "Index";
}

<h2>Index</h2>
@Model.getCount()

@foreach(var item in Model.Topics)
{
    <div>@item.header</div>
}

Output:

Index 2 test test2

Vulcano
  • 415
  • 10
  • 25
  • Thank you for this... Maybe I'm not asking the right question. I have 2 models. Entry and Topic. Originally I wanted the Entry model to contain a list of Topics so I can take the TopicId and convert it to TopicName. How do I load the Topic model from the Entry model and then how do I load my List in the Entry model? Does that make any sense? – webdad3 Aug 06 '13 at 13:03
  • I guess i know what you mean, although i'm not really sure why you want to do this. As you can create complex models for a viewmodel (eg. lists of lists of lists). Especially when you create forms & httpposts the modelbinder will automatically bind every list to the right model. Which makes your life much easier :-) But regarding your question now: as long as I don't know how the Entry and Topic look like, I can't help you directly. But take a look at LINQ: eg: http://stackoverflow.com/questions/2445410/use-linq-and-c-sharp-to-make-a-new-list-from-an-old-list – Vulcano Aug 06 '13 at 13:25
  • is there a good tutorial that you can recommend on the way you are suggesting (for the viewmodel)? I'm following some tutorials and maybe I have gone off the rails. Thanks again for your help. – webdad3 Aug 06 '13 at 13:30
  • eg.: http://www.techiesweb.net/radio-button-list-in-asp-net-mvc He shows how to create a "big" viewmodel with a list of questions and an inner list of answers. With EditorTemplates (and DisplayTemplates) you also don't need foreach loops in the view. And especially take a look how in the HttpPost he expects the model and how he iterates over it. And for your case (I still don't know what Topic and Entry are ;-) thats why I don't know why you copy the Topics to Entry and convert it and yeah. Especially if you get a changerequest like add Topic categories, you will start to love these templates – Vulcano Aug 07 '13 at 05:33
  • http://jarrettmeyer.com/post/2995732471/nested-collection-models-in-asp-net-mvc-3 a more complex tutorial - proving the same point :-) – Vulcano Aug 07 '13 at 05:52
0

It seems that you are not initializing your Topics anywhere in the code. If the collection is null it means it is not initialized. If you instantiate it with

ICollection<Topic> Topics = new List<Topic>();

Once initialized you should receive zero when calling Topics.Count. If you do not make a call to a database it will stay zero.

In your case check whether you are instantiating the Topics.

Huske
  • 9,186
  • 2
  • 36
  • 53
  • Ok that is easy enough, but how would I load that list? Is that something I can do from my Entry model? – webdad3 Aug 06 '13 at 13:24