1

I need to build a ViewModel for my Layout's Web Application, I have tried this solution but it's not based on a URL's Id coming from the URL to generate the Layout's ViewModel.

I tried this but I first had the not existing empty controller error, then I tried to include the id as a parameter but I get the error "Object reference not set to an instance of an object." because the LayoutColorRGB is not set.

public MobileController(int id)
{
    Event model = db.Events.Where(s => s.Id == id).FirstOrDefault();

    LayoutVM = new LayoutVM()
    {
        EventId = model.Id,
        LayoutColorRGB = model.LayoutColorRGB,
        SponsorLogoLink = model.SponsorLogoLink,
        SponsorLogoURL = model.SponsorLogoURL
    };

    ViewData["LayoutVM"] = LayoutVM;
}
Reza Aghaei
  • 103,774
  • 12
  • 145
  • 300
Patrick
  • 3,561
  • 11
  • 52
  • 112

2 Answers2

2

There are many cases need to extract data based on the request context and show something on layout pages. For example:

  • You may want to show logged-in user info
  • You may want to show number of visitors or online visitors
  • You you may want to show the current language and let the user to change the language.
  • You may want to load site menu or side-bar or footer from database

To do so you can consider the following points:

  • Partial View: You can create some small partial views for those parts having a specific model for each partial view and render them in the layout page.

  • Use context to get data: You can initialize the model by extracting information from Request, ViewContext, RouteData, ValueProvider and other context objects.

  • Access to data by HTML Helpers: You can create a HtmlHelper to get data from context and use the helper in the layout or partial views.

  • Access to data by dependency injection: You can define some services for extracting data and then inject those data to layout pages. In the service, you will initialize the model using context objects. If you are using ASP.NET CORE, this is a good way to go.

  • Access to data as property of base controller: You can have a property in the base controller and initialize it in constructor of the controller or in OnActionExecuting. Then in the layout, get the property by casting ViewContext.Controller to type of your base controller and read the property.

  • Access to data by ViewBag: You can initialize an instance of the model in constructor of a base controller or in OnActionExecuting method of the base controller and then put it in ViewBag. Then you can easily use it in view.

  • Layout Pages: Don't forget you can define different layout pages and use different layouts based on your requirement. You can set the layout in the action or in _ViewStart.

Example

Trying to resolve id in each request, means you need to have id as part of all requests or you need to know what should you do in absence of id. Considering this fact and to keep things simple for a minimal example, I'll define the following model and base controller and try to resolve id in OnActionExecuting method of the base controller and then will derive all my controllers which needs such behavior from this base controller.

You can do the same using an ActionFilter or a global action filter of the OnActionExecuting method of your controller.

Using the following code:

  • If you browse /home/index you will see a red bar at bottom of the page.
  • If you browse /home/index/1 you will see a blue bar at the bottom of the page
  • If you browse /home/index/2 you will see a green bar at the bottom of the page

Layout Model

public class LayoutViewModel
{
    public int? Id { get; set; }
    public string Color { get; set; }
}

Base Controller

public class BaseControllr : Controller
{
    protected override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        //Get the id from route
        var id = int.TryParse(ValueProvider.GetValue("id")?.AttemptedValue, out var temp)
            ? temp : default(int?);
        var model = new LayoutViewModel();

        //Your logic to initialize the model, for example
        model.Id = id;
        if (model.Id == null)
            model.Color = "FF0000";
         else if (model.Id%2==0)
            model.Color = "00FF00";
        else
            model.Color = "0000FF";

        //Set ViewBag
        ViewBag.MainLayoutViewModel = model;
        base.OnActionExecuting(filterContext);
    }
}

Home Controller

public class HomeController : BaseControllr
{
    public ActionResult Index(int? id)
    {
        return View();
    }
} 

_Layout.cshtml

Then in the _Layout.cshtml, add the following code before closing <body/> tag for test:

@{ 
    string color = ViewBag.MainLayoutViewModel?.Color;
    int? id = ViewBag.MainLayoutViewModel?.Id;
}
<div style="background-color:#@color;">
    Id:@id
</div>
Reza Aghaei
  • 103,774
  • 12
  • 145
  • 300
  • Hi Reza, thanks! In your opinion, do you think that it's a good practice trying to build a web application like this? I have searched a lot of possible solutions, and some say that the Layout should not have Model for example, that a based layout is not a good approach. The thing is that, having a layout that is build based in a id that will give the extra information to build the layout, is somehow a good pratice for this? – Patrick Jan 27 '20 at 17:11
  • 1
    You always need some common data to use on LayoutPages. For example You want to show logged-in user info, you want to show number of visitors or online visitors, you want to show the current language and let the user to change the language. So the requirement makes sense. – Reza Aghaei Jan 27 '20 at 18:23
  • 1
    There are several solutions to satisfy this requirement. For example you can have action filters or base controllers to extract some data from request and put into the `ViewBag`. You can do the same without `ViewBag` by having a `Property` in the base controller and get the property by casting `ViewContext.Controller` to type of your base controller and read the property. You can create a `HtmlHelper` to get data from context. You can rely on dependency injection and get the required data from services and many more options. – Reza Aghaei Jan 27 '20 at 18:25
  • 1
    @Patrick I believe the answer covers a lot of concerns and contains an example showing how you can handle the case. Adding more example IMO is out of scope of the post. Let me know if you have more question about the case or if you find the answer useful. – Reza Aghaei Feb 07 '20 at 19:32
  • I was able to put your code working. Because the information regarding the layout is in the database, I had the db.Events.Where(s => s.Id == id).FirstOrDefault(); code to the BaseController. Is this a bad practice in the BaseController? Is this a performance issue? – Patrick May 13 '20 at 22:23
  • 1
    @Patrick It depends. If it'd your requirement to show the events in all the request, it's fine in general; however in some cases you may want to use a cache mechanism. But in general don't worry, read my second comment or the first paragraph of my answer again. Hope it helps – Reza Aghaei May 14 '20 at 07:38
  • It's a requirement because the information like the color is defined by the customer in the backoffice and stored in the database. Thank You very much again for your help. – Patrick May 14 '20 at 11:03
  • 1
    You're most welcome , so if the data is like for example color or other skin/layout related data which are not likely to change frequently, you can use a cache for them, so you don't need to fetch them in every request from database. You can have a time-based invalidation of cache, or even invalidate the cache when that data changes. – Reza Aghaei May 14 '20 at 15:37
0

You should have a default value for each property of your layout. Then if your model doe not have some property you can use it from the default layout.

internal static readonly LayoutVM defaultLayout = new LayoutVM()
{
    EventId = 0,
    LayoutColorRGB = "#FFFFFF",
    SponsorLogoLink = "AnyLink",
    SponsorLogoURL = "AnyImageUrl"
};

public MobileController(int id)
{

    Event model = db.Events.Where(s => s.Id == id).FirstOrDefault();

    if (model == null)
    {
        ViewData["LayoutVM"] = defaultLayout;
        return;
    }

    LayoutVM = new LayoutVM()
    {
        EventId = model.Id,
        LayoutColorRGB = model.LayoutColorRGB ?? defaultLayout.LayoutColorRGB,
        SponsorLogoLink = model.SponsorLogoLink ?? defaultLayout.SponsorLogoLink,
        SponsorLogoURL = model.SponsorLogoURL ?? defaultLayout.SponsorLogoURL
    };

    ViewData["LayoutVM"] = LayoutVM;
}
Lutti Coelho
  • 1,610
  • 11
  • 23
  • Hi thanks! but I can't get it compiled, it generates a lot of errors "The name defaultLayout does not exist in the current context", "The modifier new is not valid for this item" etc. – Patrick Jan 22 '20 at 21:48
  • 1
    For the error **The name defaultLayout does not exist in the current context** change the private static to internal static. – Lutti Coelho Jan 22 '20 at 22:15
  • 1
    For the second error change the first line of my answer to this: internal static readonly **LayoutVM** defaultLayout = new LayoutVM() – Lutti Coelho Jan 22 '20 at 22:16
  • 1
    If yout get any other error, please update your question with full source of your controller. This way I can find the errors more easilly. Without it I'm only guessing – Lutti Coelho Jan 22 '20 at 22:17
  • 1
    Thank You Lutti for your help, I have implement @Reza's solution and it works fine. I have tried yours but I was not able to put it running, I tried several times, with different options but I was always getting errors. – Patrick May 14 '20 at 11:06