Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple controllers, one view, and one model ASP.NET MVC 3

I want to have one model & view that is served by multiple controllers in my ASP.NET MVC 3 app.

I'm implementing a system that interacts with the users' online calendar and I support Exchange, Google, Hotmail, Yahoo, Apple, ect... Each of these has wildly different implementations of calendar APIs, but I can abstract that away with my own model. I'm thinking that by implementing the polymorphism at the controller level I will be able to deal cleanly with the different APIs and authentication issues.

I have a nice clean model and view and I've implemented two controllers so far that prove I can read/query/write/update to both Exchange and Google: ExchangeController.cs and GoogleController.cs.

I have /Views/Calendar which contains my view code. I also have /Models/CalendarModel.cs that includes my model.

I want the test for which calendar system the user is using to happen in my ControllerFactory. I've implemented it like this:

public class CustomControllerFactory : DefaultControllerFactory
{
    protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
    {
        if (controllerType == typeof(CalendarController))
        {                
            if(MvcApplication.IsExchange) // hack for now
                return new ExchangeController();
            else
                return new GoogleController();
        }
        return base.GetControllerInstance(requestContext, controllerType);
    }
}

and in my Application_Start:

ControllerBuilder.Current.SetControllerFactory(new CustomControllerFactory());

This works. If I got to http://.../Calendar this factory code works and the correct controller is created!

This worked beautifully and I did it without really understanding what I was doing. Now i think I got it but I want to make sure I'm not missing something. I really spent time searching for something like this and didn't find anything.

One thing that concerns me is that I figured I'd be able to have an inheritance relationship between CalendarController and ExchangeController/GoogleController like this:

public class ExchangeController : CalendarController
{

But if I do that I get:

The current request for action 'Index' on controller type 'GoogleController' is ambiguous between the following action methods:
System.Web.Mvc.ViewResult Index(System.DateTime, System.DateTime) on type Controllers.GoogleController
System.Web.Mvc.ActionResult Index() on type Controllers.CalendarController

Which bums me out because I wanted to put some common functionality on the base and now I guess I'll have to use another way.

Is this the right way to do have multiple controllers for one view/model? What else am I going to have to consider?

EDIT: More details on my impl

Based on the responses below (thanks!) I think I need to show some more code to make sure you guys see what I'm trying to do. My model is really just a data model. It starts with this:

/// <summary>
/// Represents a user's calendar across a date range.
/// </summary>
public class Calendar
{
    private List<Appointment> appointments = null;

    /// <summary>
    /// Date of the start of the calendar.
    /// </summary>
    public DateTime StartDate { get; set; }

    /// <summary>
    /// Date of the end of the calendar
    /// </summary>
    public DateTime EndDate { get; set; }

    /// <summary>
    /// List of all appointments on the calendar
    /// </summary>
    public List<Appointment> Appointments
    {
        get
        {
            if (appointments == null)
                appointments = new List<Appointment>();
            return appointments;
        }
        set { }
    }


}

Then my controller has the following methods:

public class ExchangeController : Controller
{
    //
    // GET: /Exchange/
    public ViewResult Index(DateTime startDate, DateTime endDate)
    {
        // Exchange specific gunk. The MvcApplication._service thing is a temporary hack
        CalendarFolder calendar = (CalendarFolder)Folder.Bind(MvcApplication._service, WellKnownFolderName.Calendar);

        Models.Calendar cal = new Models.Calendar();
        cal.StartDate = startDate;
        cal.EndDate = endDate;

        // Copy the data from the exchange object to the model
        foreach (Microsoft.Exchange.WebServices.Data.Appointment exAppt in findResults.Items)
        {
            Microsoft.Exchange.WebServices.Data.Appointment a = Microsoft.Exchange.WebServices.Data.Appointment.Bind(MvcApplication._service, exAppt.Id);
            Models.Appointment appt = new Models.Appointment();
            appt.End = a.End;
            appt.Id = a.Id.ToString();

... 
        }

        return View(cal);
    }

    //
    // GET: /Exchange/Details/5
    public ViewResult Details(string id)
    {
...
        Models.Appointment appt = new Models.Appointment();
...
        return View(appt);
    }


    //
    // GET: /Exchange/Edit/5
    public ActionResult Edit(string id)
    {
        return Details(id);
    }

    //
    // POST: /Exchange/Edit/5
    [HttpPost]
    public ActionResult Edit(MileLogr.Models.Appointment appointment)
    {
        if (ModelState.IsValid)
        {
            Microsoft.Exchange.WebServices.Data.Appointment a = Microsoft.Exchange.WebServices.Data.Appointment.Bind(MvcApplication._service, new ItemId(appointment.Id));

           // copy stuff from the model (appointment)
           // to the service (a)
           a.Subject = appointment.Subject            

...
            a.Update(ConflictResolutionMode.AlwaysOverwrite, SendInvitationsOrCancellationsMode.SendToNone);

            return RedirectToAction("Index");
        }
        return View(appointment);
    }

    //
    // GET: /Exchange/Delete/5
    public ActionResult Delete(string id)
    {
        return Details(id);
    }

    //
    // POST: /Exchange/Delete/5
    [HttpPost, ActionName("Delete")]
    public ActionResult DeleteConfirmed(string id)
    {
        Microsoft.Exchange.WebServices.Data.Appointment a = Microsoft.Exchange.WebServices.Data.Appointment.Bind(MvcApplication._service, new ItemId(id));
        a.Delete(DeleteMode.MoveToDeletedItems);
        return RedirectToAction("Index");
    }

So it's basically the typical CRUD stuff. I've provided the sample from the ExchangeCalendar.cs version. The GoogleCalendar.cs is obviously similar in implementation.

My model (Calendar) and the related classes (e.g. Appointment) are what get passed from controller to view. I don't want my view to see details of what underlying online service is being used. I do not understand how implementing the Calendar class with an interface (or abstract base class) will give me the polymorphism I am looking for.

SOMEWHERE I have to pick which implementation to use based on the user.

I can either do this:

  • In my model. I don't want to do this because then my model gets all crufty with service specific code.
  • In the controller. E.g. start each controller method with something that redirects to the right implementation
  • Below the controller. E.g. as I'm suggesting above with a new controller factory.

The responses below mention "service layer". I think this is, perhaps, where I'm off the rails. If you look at the way MVC is done normally with a database, the dbContext represents the "service layer", right? So maybe what you guys are suggesting is a 4th place where I can do the indirection? For example Edit above would go something like this:

    private CalendarService svc = new CalendarService( e.g. Exchange or Google );

    //
    // POST: /Calendar/Edit/5
    [HttpPost]
    public ActionResult Edit(MileLogr.Models.Appointment appointment)
    {
        if (ModelState.IsValid)
        {
            svc.Update(appointment);
            return RedirectToAction("Index");
        }
        return View(appointment);
    }

Is this the right way to do it?

Sorry this has become so long-winded, but it's the only way I know how to get enough context across... END EDIT

like image 467
tig Avatar asked Nov 27 '11 18:11

tig


People also ask

Can two different controllers access a single view in MVC?

Yes. Mention the view full path in the View method. If the name of your Views are same in both the controllers, You can keep the Common view under the Views/Shared directory and simply call the View method without any parameter.

Can one view have multiple controllers?

Yes, It is possible to share a view across multiple controllers by putting a view into the shared folder. By doing like this, you can automatically make the view available across multiple controllers.

Can we have multiple controllers in MVC?

Yes we can create multiple controllers in Spring MVC.

Can we use more than one view for the same ViewModel in core MVC in asp net?

In MVC we cannot pass multiple models from a controller to the single view.


2 Answers

I wouldn't do it this way. As Jonas points out, controllers should be very simple and are intended to coordinate various "services" which are used to respond to the request. Are the flows of requests really all that different from calendar to calendar? Or is the data calls needed to grab that data different.

One way to do this would be to factor your calendars behind a common calendar interface (or abstract base class), and then accept the calendar into the controller via a constructor parameter.

public interface ICalendar {
  // All your calendar methods
}

public abstract class Calendar {
}

public class GoogleCalendar : Calendar {}

public class ExchangeCalendar : Calendar {}

Then within your CalendarController,

public class CalendarController {
    public CalendarController(ICalendar calendar) {}
}

This won't work by default, unless you register a dependency resolver. One quick way to do that is to use NuGet to install a package that sets one up. For example:

Install-Package Ninject.Mvc3

I think this would be a better architecture. But suppose you disagree, let me answer your original question.

The reason you get the ambiguous exception is you have two public Index methods that are not distinguished by an attribute that indicates one should respond to GETs and one to POSTs. All public methods of a controller are action methods.

If the CalendarController isn't meant to be instantiated directly (i.e. it'll always be inherited), then I would make the Index method on that class protected virtual and then override it in the derived class.

If the CalendarController is meant to be instantiated on its own, and the other derived classes are merely "flavors" of it, then you need to make the Index method public virtual and then have each of the derived classes override the Index method. If they don't override it, they're adding another Index method (C# rules, not ours) and you need to distinguish them for MVC's sake.

like image 59
Haacked Avatar answered Nov 25 '22 16:11

Haacked


I think you're on a dangerous path here. A controller should generally be as simple as possible, and only contain the "glue" between e.g. your service layer and the models/views. By moving your general calendar abstractions and vendor specific implementations out of the controllers, you get rid of the coupling between your routes and the calendar implementation.

Edit: I would implement the polymorphism in the service layer instead, and have a factory class in the service layer check your user database for the current user's vendor and instantiate the corresponding implementation of a CalendarService class. This should eliminate the need for checking the calendar vendor in the controller, keeping it simple.

What I mean by coupling to the routes is that your custom URLs is what is currently causing you problems AFAICT. By going with a single controller and moving the complexity to the service layer, you can probably just use the default routes of MVC.

like image 41
Jonas Høgh Avatar answered Nov 25 '22 15:11

Jonas Høgh