Search code examples
asp.net-mvc-4actionlinkasp.net-mvc-partialview

MVC 4 theme switching with Ajax.ActionLinks


The full text of this question is available with a screenshot here

Thanks for any help - original post follows:

So I downloaded the MvcMusicStore and fired up the completed project. I read all the articles talking about extending the view engine and using jquery plugins but I wanted to believe it could be simpler than that to just change the CSS file path when a link gets clicked. Mainly because I didn't want to copy code verbatim that I didn't fully understand. I'm very new to MVC.

So this is what I did:

To HomeController.cs I added:

    public ActionResult Theme(string themeName)
    {
        ViewBag.Theme = ThemeModel.GetSetThemeCookie(themeName);
        return View();
    }

to Models I added this class:

public class ThemeModel
{
    public static string GetSetThemeCookie(string theme)
    {
        HttpCookie cookie = HttpContext.Current.Request.Cookies.Get("userTheme");
        string rv = "Blue";
        if (theme != null)
            rv = theme;
        else
        {
            if (cookie != null)
                rv = cookie["themeName"];
            else
                rv = "Blue";
        }

        cookie = new HttpCookie("userTheme");
        HttpContext.Current.Response.Cookies.Remove("userTheme");
        cookie.Expires = DateTime.Now.AddYears(100);
        cookie["themeName"] = rv;
        HttpContext.Current.Response.SetCookie(cookie);
        return rv;

    }
}

I then created 2 copies of Site.css, changing only the background color and font-family and a view to generate my link tag.

<link href="@Url.Content(string.Format("~/Content/{0}.css", ViewBag.Theme))" rel="stylesheet" type="text/css" />

Finally, I made these changes to my _Layout.cshtml.

<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    @if (ViewBag.Theme == null) {Html.RenderAction("Theme", "Home");}

<script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" 
    type="text/javascript"></script>
</head>
<body>
<div id="header">
    <h1><a href="/">ASP.NET MVC MUSIC STORE</a></h1>
    <ul id="navlist">
        <li class="first"><a href="@Url.Content("~")" id="current">Home</a></li>
        <li><a href="@Url.Content("~/Store/")">Store</a></li>
        <li>@{Html.RenderAction("CartSummary", "ShoppingCart");}</li>
        <li><a href="@Url.Content("~/StoreManager/")">Admin</a></li>
    </ul>
</div>

@{Html.RenderAction("GenreMenu", "Store");}

<div id="main">
    @RenderBody()
</div>

<div id="footer">
    Themes: @Ajax.ActionLink("Coral", "Theme", "Home", new { themeName = "Coral" }, null, new { @style = "color : coral"} )
        @Ajax.ActionLink("Blue", "Theme", "Home", new { themeName = "Blue" }, null, new { @style = "color : blue;"})

</div>
</body>
</html>

When I run the app I get the general layout rendered twice. Once with only the genre menu rendered on the left and nothing in the body. And then again with the top 5 albums. I can't post the image as I don't have enough rep.

When I click my Coral and Blue links, my theme changes and I get just the one set without the top 5 albums.

So after some more reading on here I tried this:

_Layout.cshtml:

@{Html.RenderAction("Theme", "Home");}

HomeController.cs

    public ActionResult Theme(string themeName)
    {
        ViewBag.Theme = ThemeModel.GetSetThemeCookie(themeName);
        return PartialView();
    }

But even though this stops the duplicate rendering, when I click the theme link, the colour changes but I get absolutely nothing else on the page.

Well and truly flummoxed now and could really use some help.

Cheers, .pd.


Solution

  • Okay - here's how I did it in the end.

    Create a javascript file. Mine's called master.js:

    function ajaxSuccSetTheme(theme) {
        $('#linkTheme').attr('href', '/Content/' + theme + '.css');
    }
    

    Modify the _Layout.cshtml:

    @{
        if (ViewBag.Theme == null) {
            ViewBag.Theme = MvcMusicStore.Models.ThemeModel.GetSetThemeCookie();
        }
    }
    <link id="linkTheme" href="@Url.Content(string.Format("~/Content/{0}.css", ViewBag.Theme))" rel="stylesheet" type="text/css" />
    
    <script src="@Url.Content("~/Scripts/jquery-2.0.3.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/master.js")" type="text/javascript"></script>
    

    Notes on this:

    • The first time the page loads Theme will not have been written to the ViewBag
    • Give the <link> tag the same ID as the jQuery selector in your js file above
    • Update unobtrusive ajax jQuery file to the same version as your jQuery lib. Your Ajax.ActionLink won't work without it.

    Then my theme switching links in _Layout.cshtml look like this:

    <div id="footer">
        Themes :
            @Ajax.ActionLink("Coral", "Theme", "Home", new { themeName = "Coral" }, 
                new AjaxOptions { HttpMethod = "POST", OnSuccess = string.Format("ajaxSuccSetTheme('{0}');", "Coral")}, 
                new { @style = "color : coral;" }) | 
            @Ajax.ActionLink("Blue", "Theme", "Home", new { themeName = "Blue" }, 
                new AjaxOptions { HttpMethod = "POST", OnSuccess = string.Format("ajaxSuccSetTheme('{0}');", "Blue")}, 
                new { @style = "color : blue;" })
    </div>
    

    Notes on that:

    • themeName = "whatever" is the argument to your Theme Controller method. this gets passed to the cookie method in the ThemeModel
    • method = POST so IE doesn't cache it and I've read a couple other questions that got solved by not doing a GET
    • you have to kludge your own args to the OnSuccess js callback

    Next the HomeController.cs change:

        public ActionResult Theme(string themeName)
        {
            ViewBag.Theme = ThemeModel.GetSetThemeCookie(themeName);
            if (Request.IsAjaxRequest())
            {
                return PartialView();
            }
            else
            {
                return null;
            }
        }
    

    Honestly, it doesn't matter if you just return null without checking for IsAjaxRequest() cuz all we need from this is to set the cookie so it remembers when you next login.

    Which just leaves the cookie setting method in the ThemeModel:

    public class ThemeModel
    {
        public static string GetSetThemeCookie(string theme = null)
        {
            HttpCookie cookie = HttpContext.Current.Request.Cookies.Get("userTheme");
            string rv = "Blue";
            if (theme != null)
                rv = theme;
            else
            {
                if (cookie != null)
                    rv = cookie["themeName"];
                else
                {
                    cookie = new HttpCookie("userTheme");
                    rv = "Blue";
                }
            }
    
            cookie.Expires = DateTime.Now.AddYears(100);
            cookie["themeName"] = rv;
            HttpContext.Current.Response.SetCookie(cookie);
            return rv;
    
        }
    }
    

    Hope I helped somebody. If you'd rather do it all in jQuery here's Tim Vanfosson's Theme Manager jQuery Plugin

    Cheers,

    .pd.