ASP.NET MVC 5 - Scaffolding for Many to One Relationship

6.7k Views Asked by At

I am Working on ASP.NET MVC 5, EF 6, Razor Engine, VB Language and Database First Approach with VS 2013.

Now, in my DB; I have two tables as below:

CREATE TABLE [dbo].[Group]
(
    [Id]    INT          NOT NULL PRIMARY KEY IDENTITY(1, 1), 
    [Name]  VARCHAR(50)  NOT NULL
)

and

CREATE TABLE [dbo].[Subscriber]
(
    [Id]          INT              NOT NULL  PRIMARY KEY IDENTITY(1, 1),
    [FirstName]   [nvarchar](100)  NOT NULL,
    [MiddleName]  [nvarchar](100)  NULL,
    [LastName]    [nvarchar](100)  NOT NULL, 
    [Email]       [varchar] (200)  NOT NULL  UNIQUE, 
    [GroupId]     INT              NULL      REFERENCES [Group] ON DELETE SET NULL
)

Now, when I autogenerate the Controllers and Views using Scaffolding; I get a <select> control (with all Group items as <option> inside) in "Create Subscriber" and "Edit Subscriber" views.

But actually, I want the "Create Group" and "Edit Group" views to ask me the Subscribers I want to add in the particular group. The HTML control for the same can be a list of checkbox or <select multiple="multiple"> with all Subscriber items as <option>s.

How can I autgenerate/implement that?

1

There are 1 best solutions below

3
On BEST ANSWER

Don't rely too heavily on the scaffolding. The whole point is that it gives you a base to work from; it's not the be-all-end-all to your view. You can and should modify the scaffolding to suit your needs, and honestly, more often than not, it's easier just to start from scratch than to try to undo all the unnecessary fluff the scaffolding adds.

That said, especially when choosing multiple related items at once, you need a view model. Trying to use your entity for this is going to run out of steam fast. So create a class like:

public class GroupViewModel
{
    // `Group` properties you need to edit here

    public List<int> SelectedSubscriberIds { get; set; }

    public IEnumerable<SelectListItem> SubscriberChoices { get; set; }
}

Then, in your controller:

// We'll use this code multiple times so it's factored out into it's own method
private void PopulateSubscriberChoices(GroupViewModel model)
{
    model.SubscriberChoices = db.Subscribers.Select(m => new SelectListItem
    {
        Value = m.Id.ToString(),
        Text = m.FirstName + " " + m.LastName
    });
}

public ActionResult Create()
{
    var model = new GroupViewModel();

    PopulateSubscriberChoices(model);
    return View(model);
}

[HttpPost]
public ActionResult Create(GroupViewModel model)
{
    if (ModelState.IsValid)
    {
        // Map the posted values onto a new `Group` instance. To set `Subscribers`,
        // lookup instances from the database using the list of ids the user chose
        var group = new Group
        {
            Name = model.Name,
            Subscribers = db.Subscribers.Where(m => model.SelectedSubscriberIds.Contains(m.Id))
        };
        db.Groups.Add(group);
        db.SaveChanges()

        return RedirectToAction("Index");
    }

    PopulateSubscriberChoices(model);
    return View(model);
}

public ActionResult Edit(int id)
{
    var group = db.Groups.Find(id);
    if (group == null)
    {
        return new HttpNotFoundResult();
    }

    // Map `Group` properties to your view model
    var model = new GroupViewModel
    {
        Name = group.Name,
        SelectedSubscriberIds = group.Subscribers.Select(m => m.Id).ToList()
    };

    PopulateSubscriberChoices(model);
    return View(model);
}

[HttpPost]
public ActionResult Edit(int id, GroupViewModel model)
{
    var group = db.Groups.Find(id);
    if (group == null)
    {
        return new HttpNotFoundResult();
    }

    if (ModelState.IsValid)
    {
        group.Name = model.Name;

        // Little bit trickier here
        // First remove subscribers that are no longer selected
        group.Subscribers.Where(m => !model.SelectedSubscriberIds.Contains(m.Id))
            .ToList().ForEach(m => group.Subscribers.Remove(m));

        // Now add newly selected subscribers
        var existingSubscriberIds = group.Subscribers.Select(m => m.Id);
        var newSubscriberIds = model.SelectedSubscriberIds.Except(existingSubscriberIds);
        db.Subscribers.Where(m => newSubscriberIds.Contains(m.Id))
            .ToList().ForEach(m => group.Subscribers.Add(m));

        db.Entry(group).State = EntityState.Modified;
        db.SaveChanges()

        return RedirectToAction("Index");
    }

    PopulateSubscriberChoices(model);
    return View(model);
}

The edit post action is the most difficult. In order to not get errors about duplicate keys and such, you need to make sure you don't add duplicate items to the collection. You also need to make sure to remove the relationship between this group and any items the user has unselected. Other than that, it's pretty straight forward.

Finally in your views, you just need a to render the select list:

@model Namespace.To.GroupViewModel

...

@Html.ListBoxFor(m => m.SelectedSubscriberIds, Model.SubscriberChoices)

UPDATE

Adding converted VB code. This may not work 100% out of the box. Anyone with more VB experience may feel free to edit this to correct any issues.

View Model

Public Class GroupViewModel
    ' Group properties you need to edit here

    Public Property SelectedSubscriberIds As List(Of Integer)
    Public Property SubscriberChoices As IEnumerable(Of SelectListItem)

End Class

Controller Code

' We'll use this code multiple times so it's factored out into it's own method
Private Sub PopulateSubscriberChoices(model As GroupViewModel)
    model.SubscriberChoices = db.Subscribers.[Select](Function(m) New SelectListItem With { _
        .Value = m.Id, _
        .Text = m.FirstName & " " & m.LastName _
    })
End Sub

Public Function Create() As ActionResult
    Dim model as New GroupViewModel
    PopulateSubscriberChoices(model)
    Return View(model)
End Function

<HttpPost> _
Public Function Create(model As GroupViewModel) As ActionResult
    If ModelState.IsValid Then
        ' Map the posted values onto a new `Group` instance. To set `Subscribers`,
        ' lookup instances from the database using the list of ids the user chose
        Dim group = New Group With { _
            .Name = model.Name, _
            .Subscribers = db.Subscribers.Where(Function(m) model.SelectedSubscriberIds.Contains(m.Id)) _
        }
        db.Groups.Add(group)
        db.SaveChanges()

        Return RedirectToAction("Index")
    End If

    PopulateSubscriberChoices(model)
    Return View(model)
End Function

Public Function Edit(id As Integer) As ActionResult
    Dim group = db.Groups.Find(id)
    If group Is Nothing Then
        Return New HttpNotFoundResult()
    End If

    ' Map `Group` properties to your view model
    Dim model = New GroupViewModel With { _
        .Name = group.Name, _
        .SelectedSubscriberIds = group.Subscribers.[Select](Function(m) m.Id).ToList _
    }

    PopulateSubscriberChoices(model)
    Return View(model)
End Function

<HttpPost> _
Public Function Edit(id As Integer, model As GroupViewModel) As ActionResult
    Dim group = db.Groups.Find(id)
    If group Is Nothing Then
        Return New HttpNotFoundResult()
    End If

    If ModelState.IsValid Then
        group.Name = model.Name

        ' Little bit trickier here
        ' First remove subscribers that are no longer selected
        group.Subscribers.Where(Function(m) Not model.SelectedSubscriberIds.Contains(m.Id)).ToList().ForEach(Function(m) group.Subscribers.Remove(m))

        ' Now add newly selected subscribers
        Dim existingSubscriberIds = group.Subscribers.[Select](Function(m) m.Id)
        Dim newSubscriberIds = model.SelectedSubscriberIds.Except(existingSubscriberIds)
        db.Subscribers.Where(Function(m) newSubscriberIds.Contains(m.Id)).ToList().ForEach(Function(m) group.Subscribers.Add(m))

        db.Entry(group).State = EntityState.Modified
        db.SaveChanges()

        Return RedirectToAction("Index")
    End If

    PopulateSubscriberChoices(model)
    Return View(model)
End Function