Click here to Skip to main content
Click here to Skip to main content

MVC Basic Site: Step 3 – Dynamic Layouts and Site Admin with: AJAX, jqGrid, Controller Extensions, HTML Helpers, and more

, 25 Oct 2013 Ms-PL
Rate this:
Please Sign up or sign in to vote.
This article provides the implementation of dynamic layouts and web site administration in ASP.NET MVC4.0 by using: AJAX, jqGrid, Custom Action Results, Controller Extension, HTML Helpers, and more.

MVC Basic Site

Table of Contents

Introduction

MVC Basic Site is intended to be a series of tutorial articles about the creation of a basic and extendable web site that uses ASP.NET MVC.

The first article from this series, named MVC Basic Site: Step1-Multilingual Site Skeleton, was focused mainly in creation of a multilingual web site skeleton by using ASP.NET MVC. Also the user authentication and registration created for scratch was described there.

The second article, named MVC Basic Site: Step2-Exceptions Management, presents in details the exceptions management rules and their implementation for an ASP.NET MVC web site, and provides some utile base classes and source code for Logging and Exceptions Management that can be reused (with very small changes) not only in others ASP.NET sites by also generally in any .NET projects.

This third article from the series provides the implementation of dynamic layouts and web site administration pages by using: AJAX, jqGrid, Custom Action Results, Controller Extension, HTML Helpers and other utile C# source code and java scripts that can be extended and reused in other projects.

MVC Basic Site is developing by using an incremental and iterative methodology, this means that each new step adds more functionalities to the solution from the previous step, so the source code provided for download in this article contains all functionalities implemented until now (from all articles).

The layouts of the web sites normally include header, menus and footer. Note that these layouts are static for majority of the web sites.

Dynamic layout means that the site administrator can modify the site layout by using only the user interface provided by the web application admin pages; all changes made by the administrator will be preserved in the database and starting from that point the site layout will be changed as the site administrator wanted. So all changes of the site layout can be made anytime and from anywhere by the site administrator by using a web browser and no manually changes or settings are required.

The first part of the current article describe the building blocks used, and then the next chapters continue with the presentation of the dynamic layouts used in site administration. The last chapter gives you some hints about how to extend the dynamic layouts.

Note that all provided source code is very well commented and clean and should be no problem in reading and understanding of it. Also the building blocks used (Custom Action Results, Controller Extension, Custom HTML Helpers and other utile classes, Razor Views and Java Scripts) together with the entire site skeleton can be reused and extended by you into more complex sites.

Software Environment

  • .NET 4.0 Framework
  • Visual Studio 2010 (or Express edition)
  • ASP.NET MVC 4.0
  • SQL Server 2008 R2 (or Express Edition version 10.50.2500.0)

Custom Action Result Classes and Controller Extensions

In this chapter I will present you the custom Action Result classes and the Controller extensions used to create dynamic site layout.

In the controllers classes, each action method responds to user input by performing work and returning an action result. An action result represents a command that the framework will perform on behalf of the action method. All actions results classes must be derived from ActionResult abstract class. This abstract class has the next members:

public abstract class ActionResult
{
    // Summary:
    //     Initializes a new instance of the System.Web.Mvc.ActionResult class.
    protected ActionResult();

    // Summary:
    //     Enables processing of the result of an action method by a custom type that
    //     inherits from the System.Web.Mvc.ActionResult class.
    //
    // Parameters:
    //   context:
    //     The context in which the result is executed. The context information includes
    //     the controller, HTTP content, request context, and route data.
    public abstract void ExecuteResult(ControllerContext context);
}

There are a set of actions results available in the MVC 4.0 framework, and all of them are direct or indirect children of the ActionResult abstract class (see details about them in MSDN):

  • ContentResult
  • EmptyResult
  • FileResult
  • FileContentResult
  • FilePathResult
  • FileStreamResult
  • HttpNotFoundResult
  • HttpStatusCodeResult
  • HttpUnauthorizedResult
  • JavaScriptResult
  • JsonResult
  • JsonResult
  • RedirectResult
  • RedirectToRouteResult
  • PartialViewResult
  • ViewResultBase
  • ViewResult

For creating the dynamic site layout I have used some of the existing action result classes above (from MVC 4.0 framework), but I have created and used also the next two custom action result classes:

  • OpenFileResult
  • ImageResult

OpenFileResult

This action result class is used to open a given file into a new browser window. In our case is used to open a PDF, JPG or PNG file into a separate browser window, but can be used also for other file types.

Like you can see from the class diagram above, it has 3 properties used to set the content type, the file name and the virtual path for the file to open. The main method that does the work is the next one.

public override void ExecuteResult(ControllerContext context)
{
    context.HttpContext.Response.Clear();
    context.HttpContext.Response.ClearContent();
    //
    if(this.ContentType != null)
        context.HttpContext.Response.ContentType = ContentType;
    else
        context.HttpContext.Response.AddHeader("content-disposition", "attachment;filename=" + this.FileName);
    //
    context.HttpContext.Response.Cache.SetCacheability(System.Web.HttpCacheability.Public);
    string filePath = (_isLocal 
        ? this.FileName 
        : string.Format("{0}\\{1}", context.HttpContext.Server.MapPath(this.VirtualPath), this.FileName));
    //
    if (System.IO.File.Exists(filePath))
    {
        context.HttpContext.Response.TransmitFile(filePath);
    }
    else
    {
        context.HttpContext.Response.Write(Resources.Resource.OpenFileResultFileNotFound);
    }
    //
    context.HttpContext.Response.End();
}

Like you can see from the source code above, this method first sets up the content type, then it transmits the file to the HTTP response and in response the browser will open the file into a new browser window.

The usage of this action result should be done like in the example below:

public ActionResult GetFileResult(int id)
{
    SiteDocument SiteDocument = _db.SiteDocuments.FirstOrDefault(d => d.ID == id);
    if (SiteDocument == null)
        return RedirectToAction("Home", "Index");
    //
    OpenFileResult result = new OpenFileResult(
       SiteDocument.IsSystemDoc == null && this.Request.IsLocal, "\\Content\\Doc");
    result.FileName = SiteDocument.FileFullName;
    result.ContentType = SiteDocument.ContentType;
    //
    return result;
}

If the document is a PDF the result will be the open of the PFD into a new browser window, like you can see in Dynamic Layouts Implementation chapter.

ImageResult

This action result class is used for rendering an image from a given stream into the current view. The stream is generally and could be a memory stream that contains image loaded from the database, a file stream that contain image loaded from a file or any other input stream.

Like you can see from the class diagram above this class has two properties used to setup the contet type and the image stream. The main method that does the work is the next one.

public override void ExecuteResult(ControllerContext context)
{
    if (context == null)
        throw new ArgumentNullException("context");
    //
    try
    {
        HttpResponseBase response = context.HttpContext.Response;
        response.ContentType = this.ContentType;
        //
        if (this.ImageStream == null)
        {
            string filePath = context.HttpContext.Server.MapPath("/Content/noimageSmall.jpg");
            System.Drawing.Image imageIn = System.Drawing.Image.FromFile(filePath);
            MemoryStream ms = new MemoryStream();
            //
            imageIn.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
            response.OutputStream.Write(ms.ToArray(), 0, (int)ms.Length);
        }
        else
        {
            byte[] buffer = new byte[4096];
            //
            while (true)
            {
                int read = this.ImageStream.Read(buffer, 0, buffer.Length);
                if (read == 0)
                    break;
                //
                response.OutputStream.Write(buffer, 0, read);
            }
        }
        //
        response.End();
    }
    catch (Exception ex)
    {
        MvcBasicLog.LogException(ex);
    }
}

Like you can see from the source code above, this method first sets up the content type, then read the image from the stream, and finally write the image bytes to HTTP response output stream. In response the browser will render the image into the current view.

The usage of this action result could be done like for OpenFileResult in the chapter above, but in MVC Basic Site I am using ImageResult action result class indirectly from control extension. See details in next chapter.

Controller Extensions

The controller extensions are mechanisms that could be used in MVC to extend the controllers functionalities.

In MVC Basic site I am using ControllerExtensions class to give access in all controllers to the ImageResult custom action result described in the chapter above.

Like you can see from the class diagram above this is a static class that provide two overloading methods named Image. Note that these methods have the same return type ImageResult (our custom action result) but they have different signatures but both of them have a first special parameter of type Controller (see details below) that identify the current controller.

public static ImageResult Image(this Controller controller, Stream imageStream, string contentType)
{
    return new ImageResult(imageStream, contentType);
}

The method above is most generally and should be used to render an image of the given content type from the given image stream.

public static ImageResult Image(this Controller controller, byte[] imageBytes, string contentType)
{
    if(imageBytes == null || imageBytes.Length == 0)
        return new ImageResult( null , contentType);
    else
        return new ImageResult(new MemoryStream(imageBytes), contentType);
}

The method above should be used to render an image of the given content type from the given bytes array. The bytes array could be the image bytes that were read from the database (Image field) or could be image data received from a service via WCF (Windows Communication Foundation).

The usage of the controller extension could be done like in the example below.

public ImageResult GetHeaderImage(int id)
{
    SiteSetting shopSetting = _db.SiteSettings.First();
    //
    return this.Image(shopSetting.HeaderImage, "image/jpeg");
}

This code first load the header image data from the database then use the bytes array to render the image on the site layout. Note that the method is invoked as it belongs to the current controller and the first parameter from the method signature (of type Controller) is used indirectly.

Custom HTML Helpers

Other main building blocks that I used to implement the dynamic layouts are the custom HTML helpers. All custom helpers are contained as static methods in my RenderHelpers class.

Like you can see from the class diagram above there are a set of four different custom HTML helpers and all of them have overloaded methods used to invoke the helper with different parameters and in different context: from razor views or from controllers source code.

ImageButton

There are a set of 4 methods with the same name that implement ImageButton custom HTML helpers. These are the simplest HTML helpers created by me and used to render an image button for from the given parameters, and to associate the image button with the action of the specified controller.

public static MvcHtmlString ImageButton(this HtmlHelper htmlHelper, string altText, string imageUrl, string controllerName, 
       string action, object routeValues, object htmlAttributes = null, object linkAttributes = null)
{
    UrlHelper urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
    //
    // Create an image tag builder for the given image.
    //
    var imageBuilder = new TagBuilder("img");
    imageBuilder.MergeAttribute("src", urlHelper.Content(imageUrl));
    imageBuilder.MergeAttribute("alt", altText);
    imageBuilder.MergeAttribute("title", altText);
    imageBuilder.MergeAttributes(new RouteValueDictionary(htmlAttributes));
    //
    // Create a link tag builder that use the image tag builder!
    //
    var linkBuilder = new TagBuilder("a");
    linkBuilder.MergeAttribute("href", urlHelper.Action(action, controllerName, routeValues));
    linkBuilder.MergeAttributes(new RouteValueDictionary(linkAttributes));
    linkBuilder.InnerHtml = imageBuilder.ToString(TagRenderMode.SelfClosing);
    //
    return MvcHtmlString.Create(linkBuilder.ToString(TagRenderMode.Normal));
}

This method is designed to be invoked from a razor view, it has the maximum number of parameters and is the method that do the work. It renders an image button for from the given parameters and associates it with the action of the specified controller (should be a second controller different from the current controller).

Like you can see from the source code above this method creates an image tag from the given parameters into an anchor tag. The given HTML attributes are used only for image tag, and the link attributes are used only in anchor tag.

Note that this method is used by all the others overloaded ImageButton methods described below to render the image button.

public static string ImageButton(Controller controller, string altText, string imageUrl, 
       string action, object routeValues, object htmlAttributes = null, object linkAttributes = null)
{
    HtmlHelper htmlHelper = new HtmlHelper(
        new ViewContext(controller.ControllerContext,
                new WebFormView(controller.ControllerContext, action),
                controller.ViewData,
                controller.TempData,
                TextWriter.Null),
        new ViewPage());
    //
    return ImageButton(htmlHelper, altText, imageUrl, action, routeValues, htmlAttributes, linkAttributes).ToHtmlString();
}

The code above renders an image button from the given parameters and associates it with an action of the current controller. This method is designed to be used from the current controller.

public static MvcHtmlString ImageButton(this HtmlHelper htmlHelper, string altText, string imageUrl, 
       string action, object routeValues, object htmlAttributes = null, object linkAttributes = null)
{
    return ImageButton(htmlHelper, altText, imageUrl, null, action, routeValues, htmlAttributes, linkAttributes);
}

The code above renders an image button from the given parameters and associates it with an action of the current controller. This method is designed to be used from a razor view.

public static string ImageButton(Controller controller, string altText, string imageUrl, string controllerName,
       string action, object routeValues, object htmlAttributes = null, object linkAttributes = null)
{
    HtmlHelper htmlHelper = new HtmlHelper(
        new ViewContext(controller.ControllerContext,
                new WebFormView(controller.ControllerContext, action),
                controller.ViewData,
                controller.TempData,
                TextWriter.Null),
        new ViewPage());
    //
    return ImageButton(htmlHelper, altText, imageUrl, controllerName, action, 
           routeValues, htmlAttributes, linkAttributes).ToHtmlString();
}

The code above renders an image button from the given parameters and associates it with an action of a second controller. This method is designed to be used from the current controller but is use the action of a second controller.

The usage of the ImageButon HTML helper from the razor view should be done like in the next example:

@Html.ImageButton(Resource.ViewTip, 
    "~/Content/view.png", 
    "GetFileResult", 
    new { id = host.ID }, 
    new { style = "border:0px;" }, 
    new { target = "blank_" })

Note that the “@Html” sentence is used to invoke the custom HTML helper and the MVC framework will identify the right method of the RenderHelpers class and will send them automatically the current controller. Note that the last parameter is setup the HTML property “target” with the value “blank_”, so when the user will click on this image button the file with the specified ID, returned by the GetFileResult action (for example a PDF file from the server), will be opened by the browser into a new window. Note that the tool tip for the button (the first parameter) is read from the resources files and is multilingual.

RenderHelpers.ImageButton(this,
    Resource.ViewTip, 
    "~/Content/view.png", 
    "GetFileResult", 
    new { id = host.ID }, 
    new { style = "border:0px;" }, 
    new { target = "blank_" })

Note that in the source code above the action GetFileResult belong to a second controller named SiteDocumentControler that is different from the current controller. Also in this case I am using in plus a new parameter, the current controller sent as first parameter.

The source code above renders image button with tool tip like in the next screenshot:

EnumDropDownList

There are a set of 2 methods with the same name that implement EnumDropDownList custom HTML helpers and they render a dropdown list for a generic TEnum and the other given parameters.

public static MvcHtmlString EnumDropDownList<TEnum>(this HtmlHelper htmlHelper, string name, string action, 
    TEnum selectedValue, bool isReadOnly = false)
{
    //
    // Create a list of SelectListItem from all values of the given enum.
    //
    IEnumerable<TEnum> values = Enum.GetValues(typeof(TEnum)).Cast<TEnum>();
    IEnumerable<SelectListItem> items = from value in values
        select new SelectListItem
            {
                Text = value.ToString(),
                Value = value.ToString(),
                Selected = (value.Equals(selectedValue))
            };
    //
    // Render the drop down list by using the list created above.
    //
    if (isReadOnly)
    {
        return MvcHtmlString.Create(htmlHelper.DropDownList(
            name,
            items,
            null,
            new
            {
                @disabled = "disabled",
                style = "color: #999999;readonly:true;",
            }
            ).ToString());
    }
    else
    {
        return MvcHtmlString.Create(htmlHelper.DropDownList(
            name,
            items,
            null,
            new
            {
                onchange = string.Format(
"window.location='/{0}?value='+this.options[this.selectedIndex].value+ '&id='+ $(this).parent().parent()[0].id"
, action)
            }
            ).ToString());
    }
}

Like you can see from the source code above this method creates a drop down list and fills it with all values of the given. It selects the specified selected value and it use java script to invoke the action (specified as parameter) with the selected value of the enum when the current selected item from the drop down list is changed.

The last parameter of the method, named isReadOnly, is optionally, and when is set to true a disabled and read only drop down list will be rendered.

Note that this method is used by the other overloaded EnumDropDownList method described below to render a drop down list for the given parameters.

public static string EnumDropDownList<TEnum>(Controller controller, string name, string action, 
       TEnum selectedValue, bool isReadOnly = false)
{
    HtmlHelper htmlHelper = new HtmlHelper(
        new ViewContext(controller.ControllerContext,
                new WebFormView(controller.ControllerContext, action),
                controller.ViewData,
                controller.TempData,
                TextWriter.Null),
        new ViewPage());
    //
    return EnumDropDownList<TEnum>(htmlHelper, name, action, selectedValue, isReadOnly).ToHtmlString();
}

The code above renders a drop down list from the given parameters and associates it with an action of a controller. This method is designed to be used from the current controller source code.

The usage of the EnumDropDownList HTML helper from a controller source code should be done like in the next example:

Culture = RenderHelpers.EnumDropDownList(this,
    "dropDown",
    "SiteDocument/SetCultureInfo",
    host.Culture != null ? (SiteCultures)host.Culture : SiteCultures.All,
    host.IsSystemDoc == null ? false : true);

The source code above invoked from a set of values, renders drop down lists like in the next screenshot:

Note that in the source code above only the current selected value from the enum is given and based on its type the generic TEnum will be replaced with the current used enum. Also the action value is “SiteDocument/SetCultureInfo”; so it must be an URL that contains the controller name followed by the action name. Note that the action SetCultureInfo must have one a signature like in the next one:

public ActionResult SetCultureInfo(string value, string id)

When the user will change the drop down list selected item, this action will be invoked and will receive as parameters the value and the ID of the current selected item from the drop down list ( in our case the enum text and its number).

CustomCheckBox

There are a set of two methods with the same name that implement CustomCheckBox custom HTML helpers and they render a check box for the other given parameters.

public static MvcHtmlString CustomCheckBox(this HtmlHelper helper, string name, string value, 
    string action, bool isReadOnly, object htmlAttributes = null)
{
    TagBuilder builder = new TagBuilder("input");
    //    
    if (Convert.ToInt32(value) == 1) 
        builder.MergeAttribute("checked", "checked");
    //
    if (isReadOnly)
    {
        htmlAttributes = new
        {
            @disabled = "disabled",
            style = "color: #999999;readonly:true;",
        };
    }
    else
    {
        htmlAttributes = new
        {
            style = "margin-left:auto; margin-right:auto;",
            onchange = string.Format(
"window.location='/{0}?rowid=' +$(this).parent().parent()[0].id + '&value='+$(this).val()"
, action)
        };
    }
    //
    builder.MergeAttributes(new RouteValueDictionary(htmlAttributes));
    builder.MergeAttribute("type", "checkbox");
    builder.MergeAttribute("name", name);
    builder.MergeAttribute("value", value);
    //
    return MvcHtmlString.Create(builder.ToString(TagRenderMode.SelfClosing));
}   

This method is designed to be invoked from a razor view, it has the maximum number of parameters and is the method that do the work.

Like you can see from the source code above this method render a check box and associates it with an given controller action by using java script. The penultimate parameter of the method, named isReadOnly, when is set to true a disabled and read only check box will be rendered.

Note that this method is used by the other overloaded CustomCheckBox method described below to render a check box for the given parameters.

public static string CustomCheckBox(Controller controller, string name, string action, string value, 
    bool isReadOnly, object htmlAttributes = null)
{
    HtmlHelper htmlHelper = new HtmlHelper(
        new ViewContext(controller.ControllerContext,
                new WebFormView(controller.ControllerContext, action),
                controller.ViewData,
                controller.TempData,
                TextWriter.Null),
        new ViewPage());
    //
    return CustomCheckBox(htmlHelper, name, value, action, isReadOnly, htmlAttributes).ToHtmlString();
}

The code above renders a check box from the given parameters and associates it with an action of a controller. This method is designed to be used from the current controller source code.

The usage of the CustomCheckBox HTML helper from a controller source code should be done like in the next example:

IsNotPublic = RenderHelpers.CustomCheckBox(this, 
    "checkBox1",
    "SiteDocument/SetIsNotPublic",
    host.IsNotPublic != null && host.IsNotPublic == true ? "1" : "0", 
    host.IsSystemDoc == null ? false : true);

The source code above invoked from a set of values, renders drop down lists like in the next screenshot:

ImageFromStream

There are a set of four methods with the same name that implement ImageFromStream custom HTML helpers.

public static MvcHtmlString ImageFromStream(this HtmlHelper helper, string altText, string controllerName, 
    string action, int imageID, object htmlAttributes = null)
{
    if (imageID > 0)
    {
        UrlHelper urlHelper = new UrlHelper(helper.ViewContext.RequestContext);
        //
        // Create an image tag builder for the given image.
        //
        var imageBuilder = new TagBuilder("img");
        imageBuilder.MergeAttribute("src", (controllerName == null 
            ? urlHelper.Action(action, new { ID = imageID }) 
            : urlHelper.Action(action, controllerName, new { ID = imageID })));
        //
        if (altText != null)
        {
            imageBuilder.MergeAttribute("alt", altText);
            imageBuilder.MergeAttribute("title", altText);
        }
        //
        imageBuilder.MergeAttributes(new RouteValueDictionary(htmlAttributes));
        //
        return MvcHtmlString.Create(imageBuilder.ToString(TagRenderMode.SelfClosing));
    }
    else
    {
        //
        // For invalid image ID return an empty string.
        //
        TagBuilder brTag = new TagBuilder("br");
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.Append("");
        stringBuilder.Append(brTag.ToString(TagRenderMode.SelfClosing));
        //
        return MvcHtmlString.Create(stringBuilder.ToString());
    }
}

This method is designed to be invoked from a razor view, it has the maximum number of parameters and is the method that do the work. It renders an image from stream and for the others given parameters. The image data are read by the action of the specified controller and by the given image ID (in our case the data is loaded from the database but image could be also from server folder).

Like you can see from the source code above this method creates an image tag from the given parameters. The URL used to get the image data is built by using the given controller action. The given HTML attributes are also used only for the image tag.

Note that this method is used by all the others overloaded ImageFromStream methods described below to render the image.

public static string ImageFromStream(Controller controller, string altText, 
    string action, int imageID, object htmlAttributes = null)
{
    HtmlHelper htmlHelper = new HtmlHelper(
        new ViewContext(controller.ControllerContext,
                new WebFormView(controller.ControllerContext, action),
                controller.ViewData,
                controller.TempData,
                TextWriter.Null),
        new ViewPage());
    //
    return ImageFromStream(htmlHelper, altText, action, imageID, htmlAttributes).ToHtmlString();
}

The code above renders an image from the given parameters and associates it with an action of the current controller to load the image data. This method is designed to be used from the current controller.

public static MvcHtmlString ImageFromStream(this HtmlHelper helper, string altText,
    string action, int imageID, object htmlAttributes = null)
{
    return ImageFromStream(helper, altText, action, null, imageID, htmlAttributes);
}

The code above renders an image button from the given parameters and associates it with an action of the current controller to load the image data. This method is designed to be used from a razor view.

public static string ImageFromStream(Controller controller, string altText, string controllerName,
    string action, int imageID, object htmlAttributes = null)
{
    HtmlHelper htmlHelper = new HtmlHelper(
        new ViewContext(controller.ControllerContext,
                new WebFormView(controller.ControllerContext, action),
                controller.ViewData,
                controller.TempData,
                TextWriter.Null),
        new ViewPage());
    //
    return ImageFromStream(htmlHelper, altText, action, imageID, htmlAttributes).ToHtmlString();
}

The code above renders an image button from the given parameters and associates it with an action of the current controller to load the image data. This method is designed to be used from a razor view.

The usage of the ImageFromStream HTML helper from the razor view should be done like in the next example:

@Html.ImageFromStream("Home", 
    "SiteSetting", 
    "GetHeaderImage", 
    7, 
    new { id = "_leftImage" })

Note that the “@Html” sentence is used to invoke the custom HTML helper and the MVC framework will identify the right method of the RenderHelpers class and will send them automatically the current controller. Note that the last parameter is setup the HTML property “id” with the value “leftImage” and there is some CSS styles associated with this ID. The image is read by using the action “GetHeaderImage” of the controller SiteSettingController and the image data in our case is read from the database.

The source code above renders the page header image like in the next screenshot:

jqGrid Integration in MVC4.0 by using AJAX, JSON, jQuery, LINQ and Serializations

jqGrid is an open source AJAX-enabled JavaScript control that provides solutions for representing and manipulating tabular data on the web, and that loads the data dynamically through AJAX callbacks.

The documentation and examples about jqGrid can be found on the next site: http://www.trirand.com

The integration of the jqGrid in our MVC Basic Site is done by using AJAX, JSON, jQuery, Java Script, Linq and Serializations. Because this integration is a big part it I will present it in details into the next MVC Basic Site article, but the source code is already in the current solution.

A screenshot with the results of this integration in shown below in the “Visitors” page from administrator area (you must login with user Administrator and password tm77dac).

Like you can see from the screenshot above, the grid from the page has columns of different types (strings, date time and Boolean), and all columns have sorting enabled. The last column, named “Actions” use my ImageButton custom HTML helper (described above); when the user press on “Delete” image button (from action column) the associated visitor log entry will be deleted from the database.

Note that there is a paging controller in the bottom part of the grid and there are also two action buttons: “Reload Grid” button (image button) and “Delete All” button -used to delete all visitors log histories for the current filter.

Dynamic Layouts and Site Admin

This chapter presents in details the implementation of dynamic layouts and web site administration by using all building blocks presented above (Custom Action Results, Controller Extension, HTML Helpers, AJAX and jqGrid).

First I will make a short presentation of the data entities used for the site data, then I will continue with the presentation of the site admin pages and their controllers classes used to administrate the dynamic layouts, and finally I will preset you details about the dynamic layout implementation.

Data Entitities

The image above is the data entities diagram used by the MVC Basic Site solution. There are 6 entities and each of them is associated with an SQL database table with similar name. The next 3 entities from the diagram above: Country, Address and User were already described in MVC Basic Site – Step1 article.

There are three new entities used for site admin and dynamic layouts:

  • VisitorLog: stores the site visitors log entries; for each new user site usage the next information are stored: UserID, start date, end date and was time out flag.
  • SiteSetting: sores the current site settings; these include the next information: contact ID (linked with the address for contact from Address table), contact email, support email, header description, header image and site title. Note that this entity stores information used for dynamic site layouts.
  • SiteDocument: used to stores the site documents data. A site document is a file that can be uploaded dynamically by the system administrator and associated with specific culture info or with all culture info. For each site document the next information are stored: name (shown in the site layout), file full name (the path of the document file on the server), culture (the document is accessible for all cultures or only for one specified culture), is not public flag (not public documents can be viewed only by authenticated users), is system document flag (system documents cannot be edited or deleted), is for agreement flag (the document is used for site agreement for a specific culture), date (the upload date). Note that this entity stores information used for dynamic site layouts.

Site Settings

The site main settings are managed by the site administrator by using Settings page.

Like you can see from the image above, by using Settings page, the administrator can setup the data stored in the SiteSetting entity described above.

So in this way the contact data and support email could be edited, but also the next main data used in dynamic layouts:

  • Header Image: the administrator has to possibilities: to use the default image, or to specify a new header image by selecting and uploading the desired image;
  • Header Description: the header description that will be shown in site layouts;
  • Pages Title: the pages title that will be shown in the browser as prefix for page title, like in the next example: “Basic Site – Settings”.

After the user modify the site settings like in the image above the result will be, that the new header image and description will be used form that points in all site layouts, like you can see in the screenshot below.

Note that in the screenshot above, the site admin layout uses the new header image and header description. But if you logout you will see that also the main (users) layout will use the new site settings.

SiteSeetingController is derived from BaseController and is the controller class that manages the actions from Settings page.

You can see in the class diagram above the actions used in Settings page and also the properties HasHeaderImage and SiteTitle and the method GetHeaderImage() used in the layouts headers to read the site settings info. You can see more details in the source code.

Site Documents

A site document is a document associated with a file that is uploaded dynamically to the server by the site administrator and then, based on the document properties, user type (anonymous or authenticated user), current culture used (English, Română, Deutsch) can be accessed and viewed by the user from the main layout footer.

In the current implementation the document file can be a PDF, TXT, HTM, HTML, PNG or JPG file, but other types could be added.

Not that if a document is not public, this means that only the authenticated user can view the document, and if a document is for a specific culture the user can see that document only when he/she is using that culture.

The site documents are managed by the site administrator by using Documents page.

In the screenshot above you can see the site documents data into a jqGrid with sortable columns.

Note that the cells from the next grid columns: Culture, Not Public, For Agreement, and Actions are using for rendering my custom HTML helpers described above.

The site administrator can add dynamically as many documents he/she want by using the upload controls from the bottom of the page. Note that for each new document its data will be stored into the database and the file will be saved on the server on the site Content\Doc\folder.

There are 3 “system” documents in the grid named SiteAgreement-DE.htm, SiteAgreement-RO.htm, and SiteAgrement-EN.htm that are used for site agreement for the associated culture. These documents are part of the system and are the default site agreement documents used for the specified culture, so they cannot be deleted, but they can be edited. The administrator can setup other document for site agreement for a culture or for all cultures, and from that point that document will be used for the site agreement and not the default one.

Based on the document type (PDF, JPG and PNG files cannot be editable) there is a set of actions for each document in the last column. The system document cannot be deleted by they can be edited. The PDF documents can be deleted or viewed, and the TXT, HTM and HTML documents can be edited or deleted.

For example if you want to edit the site agreement for English culture you have to press on the associated “Edit” image button and the agreement file will be opened for editing into “Edit” page.

In the screenshot above you can see that I changed the site agreement for “English” culture by adding a new paragraph.

After saving the changes, if I logout and from the Home page I will use the “English” culture, then from the Home page footer I press on the “Site-Agreement” document, the document will be opened into the site like in the screenshot below.

In the screenshot above you can see that the new added paragraph is now in the site agreement document.

Note that the user is not authenticated so it has access only to public documents and to the documents that are for “All” cultures and for “English” culture.

Now if the user login by using username Ana and password ana, then change the current culture to “Romănă”, the user will have access (in the pages footer) to more documents, and some documents are different then in the first case, like you can see in the screenshot below.

In the picture above you can see that the current user is authenticated, so it has access to not public documents, and he/she is using the Romanian culture, so has access to site documents that were defined to be used in Romanian culture. In the current page the “Contact” information (from site settings) are shown.

Note that in all cases the first two documents from the main layout footer are special. The first one display the contact information (a static page with dynamic content) and the second one is the current site agreement document for the current used culture.

If the user press on a document of type TXT, HTML, or HTM the document will be opened directly in the site, like we already seen in the case of agreement document; but if the document is PDF, JPG or PNG the document will be opened into other browser window so the user will can view it, print it and/or save it on his/her computer, like in the picture below.

The user just clicked on the site document named “icon-sd” and the associated PDF document was opened into a new browser window, and now the user can view, print, email and/or save the document.

SiteDocumentController is derived from BaseController and is the controller class that manages the actions from admin “Site Documents” pages and also the actions related with “Site Documents” invoked from main layout at user request.

In the class diagram above you can see all methods of the SiteDocumentController. More details are provided in the source code and in the next chapter.

Dynamic Layouts Implementation

In this chapter I will present you the dynamic layouts implementation details.

In the current version, MVC Basic Site is using the next two layouts:

  • _AdminLayout.cshtml -used only for administrator pages;
  • _Layout.cshtml –the main layout used for user pages;

Both layouts have using the same header implemented by _Header.cshtml partial page, but they have different menus and footers.

The site documents, that can be setup dynamically by the system administrator, are rendered dynamically in the main layout footer like you can see in the razor code below.

<tr>
    <td id="headerLeftImage">
        @if (MvcBasicSite.Controllers.SiteSettingController.HasHeaderImage)
        {
            @Html.ImageFromStream("Home", "SiteSetting", "GetHeaderImage", 7, new { id = "_leftImage" })
        }
        else
        {
            <img id="_leftImage" src="@Url.Content("~/Content/HeaderLogo.png")"/>
        }
    </td>
    <td>
        <div class="headerTitle">
             
            @MvcBasicSite.Controllers.SiteSettingController.HeaderDescription
        </div>
        @if (!(Model is LogOnModel))
        {
            <div class="errorMessage">
                @Html.ValidationSummary(true)
            </div>
        }
    </td>
</tr>

Note that I am using ImageFromStream, my custom HTML helper described above, to render the image by using the data returned by the SiteSettingControler.

The site documents, that can be setup dynamically by the system administrator, are rendered dynamically in the main layout footer by the razor code below.

@if(siteDocuments != null)
{   
    int j, m = siteDocuments.Length;
    //
    // 1st column.
    //
    @:<ul class="column first"><li><a href="#">@Html.ActionLink(
       Resources.Resource.HomeContact, "Contact", "Home")</a></li>
    if (m > 3)
    {
        for (j = 3; j < m; j += 4)
        {
            SiteDocument doc = siteDocuments[j];
            if (doc.IsEditable)
            {
                @:<li><a href="#">@Html.ActionLink(doc.Name, 
                  "Details", "SiteDocument", new { id = doc.ID }, null)</a></li>
            }
            else
            {
                @:<li><a href="#">@Html.ActionLink(doc.Name, 
                  "GetFileResult", "SiteDocument", 
                  new { id = doc.ID }, new { target = "_blank" })</a></li>
            }
        }
    }
    @:</ul>
    //
    // 2nd column.
    //
    @:<ul class="column">
    if (m > 0)
    {
        for (j = 0; j < m; j += 4)
        {
            SiteDocument doc = siteDocuments[j];
            if (doc.IsEditable)
            {
                @:<li><a href="#">@Html.ActionLink(doc.Name, 
                  "Details", "SiteDocument", new { id = doc.ID }, null)</a></li>
            }
            else
            {
                @:<li><a href="#">@Html.ActionLink(doc.Name, "GetFileResult", 
                  "SiteDocument", new { id = doc.ID }, new { target = "_blank" })</a></li>
            }
        }
    }
    @:</ul>
    //
    // 3rd column.
    //
    if (m > 1)
    {
        @:<ul class="column">
        for (j = 1; j < m; j += 4)
        {
            SiteDocument doc = siteDocuments[j];
            if (doc.IsEditable)
            {
                @:<li><a href="#">@Html.ActionLink(doc.Name, "Details", 
                  "SiteDocument", new { id = doc.ID }, null)</a></li>
            }
            else
            {
                @:<li><a href="#">@Html.ActionLink(doc.Name, 
                  "GetFileResult", "SiteDocument", new { id = doc.ID }, 
                  new { target = "_blank" })</a></li>
            }
        }
        @:</ul>
    }
    //
    // 4th column.
    //
    if (m > 2)
    {
        @:<ul class="column">
        for (j = 2; j < m; j += 4)
        {
            SiteDocument doc = siteDocuments[j];
            if (doc.IsEditable)
            {
                @:<li><a href="#">@Html.ActionLink(doc.Name, "Details", 
                  "SiteDocument", new { id = doc.ID }, null)</a></li>
            }
            else
            {
                @:<li><a href="#">@Html.ActionLink(doc.Name, "GetFileResult", 
                  "SiteDocument", new { id = doc.ID }, new { target = "_blank" })</a></li>
            }
        }
        @:</ul>
    }
}	

Note that in the razor code above the site documents are rendered dynamically in 4 columns. The first position form the first row is used not for a site document, but to open the “Contact” page. The second position is always used for the current site agreement, because the current site agreement for the current user and for the current culture is always added in the second position when the site documents data are read from the database.

Note that depends if the document is an editable document (TXT, HTML or HTM) or not (PDF, JPG, or PNG) Details or GetFileResult action of the SiteDocumentController is used to open the document directly in the site or into a new browser page.

public ActionResult Details(int id)
{
    SiteDocument SiteDocument = _db.SiteDocuments.Single(u => u.ID == id);
    if (SiteDocument == null)
        return RedirectToAction("Home", "Index");
    //
    try
    {
        string filePath = (SiteDocument.IsSystemDoc == null && this.Request.IsLocal
            ? SiteDocument.FileFullName
            : Path.Combine(HttpContext.Server.MapPath("/Content/Doc"), 
               Path.GetFileName(SiteDocument.FileFullName)));
        //
        SiteDocument.FileData = System.IO.File.ReadAllText(filePath);
    }
    catch (Exception ex)
    {
        MvcBasicLog.LogException(ex);
        ModelState.AddModelError("", ex.Message);
    }
    //
    return View(SiteDocument);
}

The source code above will be invoked at the user request (click on the document from the footer) and will read the data from the server for an editable document, then will show the document data into the site Details page. Note that Request.IsLocal property is used to can test from Visual Studio.

public ActionResult GetFileResult(int id)
{
    SiteDocument SiteDocument = _db.SiteDocuments.FirstOrDefault(d => d.ID == id);
    if (SiteDocument == null)
        return RedirectToAction("Home", "Index");
    //
    OpenFileResult result = new OpenFileResult(SiteDocument.IsSystemDoc == null && this.Request.IsLocal, "\\Content\\Doc");
    result.FileName = SiteDocument.FileFullName;
    result.ContentType = SiteDocument.ContentType;
    //
    return result;
}

The source code above is will be invoked at the user request (click on the document from the footer) and will read the data from the server for an non editable document, then will use OpenFileResult (my custom Action Result described above) to send a PDF, IMG or JPG image to the browser to be open into a new browser window.

Note that, before to be used in the main layout, the site documents data are read from the database and cached in the Index method of the HomeController class:

public ActionResult Index()
{
    if(Session["siteDocuments"] == null)
    {
        //
        // Load and cache the site documents for the current user.
        //
        SiteSession siteSession = this.CurrentSiteSession;
        User user = (siteSession == null ? null : _db.Users.FirstOrDefault(u => u.ID == siteSession.UserID));
        //
        SiteDocument[] shopDocuments = SiteDocument.GetSiteDocumentsForUser(_db, user, SiteSession.CurrentUICulture);
        Session["siteDocuments"] = shopDocuments;
    }
    // TO DO!
    return View();
}

Because the site documents depends on the current user and on the current selected culture, in the source code above first will get the current site session, then we read the current user and use also the current selected culture to read the site documents data from the database (see details below). The returned results are then cached into the session cache and then used in the main layout.

In the source code below the SiteCultures enum value are used, so here are the details:

public enum SiteCultures : int
{
    All = -1,
    English = 0,
    Romana = 1,
    Deutsh = 2,
}

The source code above defines the site cultures and their associated integer values. Note that for SiteDocument entity Culture property the integer values will be used.

public static SiteDocument[] GetSiteDocumentsForUser(MvcBasicSiteEntities dataContext, User user, int culture)
{
    List<sitedocument> resultsList = (user != null
        ? dataContext.SiteDocuments.Where(d => d.IsForAgreement == null && 
         (d.Culture == -1 || d.Culture == culture)).OrderBy(d => d.ID).ToList()
        : dataContext.SiteDocuments.Where(d => d.IsForAgreement == null && 
         (d.IsNotPublic == null || d.IsNotPublic == false) 
            && (d.Culture == -1 || d.Culture == culture)).OrderBy(d => d.ID).ToList());
    //
    SiteDocument document = GetShopAgreementDocument(dataContext, culture);
    if (document != null)
        resultsList.Insert(0, document);
    //
    return resultsList.ToArray();
}</sitedocument>

In the application logic layer, the source code above uses LINQ to read from the database the shop documents that the current user has rights to see and insert always in the first position the site agreement document for the given parameters.

public static SiteDocument GetShopAgreementDocument(MvcBasicSiteEntities dataContext, int culture)
{
    SiteDocument SiteDocument = dataContext.SiteDocuments.Where(d => d.IsForAgreement == true 
        && d.IsSystemDoc == null && (d.Culture == -1 || d.Culture == culture)).FirstOrDefault();
    if (SiteDocument != null)
        return SiteDocument;
    else
        return dataContext.SiteDocuments.Where(d => d.IsForAgreement == true 
            && (d.Culture == -1 || d.Culture == culture)).FirstOrDefault();
}

The source code above uses LINQ to read the shop agreement document for the current given culture. It looks if there exists a site agreement document defined by the administrator to be used in place of the default one; if exist such document that document will be used, otherwise the default site agreement document (system document) for the given culture info will be used.

How to Extend the Dynamic Layouts

The dynamic layouts implementation provided in this article can be extended in the next ways:

  1. Add more settings in the database Settings table, then extend the Settings admin page to allow the edit of them, and finally show more information based on them in the Contact page and/or in the web site layouts.
  2. Use the existing documents type (TXT, HTML, PDF, IMG, PNG) in other ways then in the provided solution. For example the links for opening of the site documents can be added dynamically from your site layouts menus. In this way new HTML pages can be added dynamically to your site by the site administrator, to show news for example.
  3. Add documents of types of type CSS, and use them in the site layouts, so the CSS of the site could be modified dynamically in this way.       

    For doing this, first you must modify in the class SiteDocument the next properties: IsEditable and ContentType, then you should add in the SiteDocuments table one or more “system documents” for your CSS files, then finally use the CSS in your site layouts.

  4. Add more documents types and use them in your layouts as I indicated in this article and/or as you wanted.

    For doing this you should fallows the steps indicate in point 3) above.

Before Running this Code

Before running this code, you should do the next steps:

  1. Create a new entry into the Event Log by running CreateEventLogEntry application as Administrator (CreateEventLogEntry application source code is provided as part of our solution);
  2. Create a database named MvcBasicSite into your SQL Server (or SQL Express), then restore the provided database MvcBasicSiteDatabase.bak on it.
  3. Modify the connection string into the Web.config file of the MvcBasicSite web application according to your settings from step 2.

If this article and provided source code is helpful for you, you could vote it; if you have questions I am here to help you!

References

History

  • 11thApril, 2013: Version 1.0.0.1 - Draft Version.
  • 12thApril, 2013: Version 1.0.0.2 - Release Version.
  • 19thApril, 2013: Version 1.0.1.1 - New chapters added.
  • 28thApril, 2013: Version 1.0.1.2 - New info added.
  • 18thMay, 2013: Version 1.0.1.2 - Update MVC Basic Steps.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)

Share

About the Author

Raul Iloc
Architect Nagarro Software SR
Romania Romania
I am a software project manager with about 19 years working experience in software development and about 14 years as Project Manager and Architect.
 
I am working at Nagarro Software SRL (the former SoftCon IT Services SRL), member of Nagarro division (www.nagarro.com) a subsidiary of German-based Allgeier Holding AG (www.allgeier.com) - an integrated IT service group with more than 5,200 salaried employees and around 1,500 freelance IT experts.
 
I am Project Manager and Software Architect for .NET Development and I am responsible for the management of: the software development projects, the projects teams (building, mentoring, leading and supervising), the requirements, the deliverables, the estimations of the projects costs, the project plans, the risk plans, and others project management tasks. I play also the role of lead developer for the software products developed by my teams.
Follow on   LinkedIn

Comments and Discussions

 
GeneralMy vote of 2 PinmemberOscar R. Onorato25-Nov-14 6:14 
AnswerRe: My vote of 2 PinmvpRaul Iloc25-Nov-14 19:58 
SuggestionAdd SQL database script Pinmemberniravmmehta24-Jul-14 20:19 
GeneralMy vote of 5 PinmemberLee Robie11-Jul-13 10:21 
GeneralRe: My vote of 5 PinprofessionalRaul Iloc24-Jul-13 20:15 
Thank you for your vote and appreciation!
Raul Iloc

GeneralMy vote of 5 PinmemberSandeep Ramani5-Jul-13 0:25 
GeneralRe: My vote of 5 PinprofessionalRaul Iloc24-Jul-13 20:15 
GeneralMy vote of 5 PinmemberLisandru8813-Jun-13 12:17 
GeneralRe: My vote of 5 PinprofessionalRaul Iloc13-Jun-13 19:44 
QuestionRestoring to a lower version of SQL Server failed PinmemberAmin J. Ismaili15-May-13 6:41 
AnswerRe: Restoring to a lower version of SQL Server failed PinprofessionalRaul Iloc15-May-13 20:28 
QuestionMy vote of 5 PinmemberMoises Barba14-May-13 23:41 
AnswerRe: My vote of 5 PinprofessionalRaul Iloc15-May-13 2:52 
AnswerRe: My vote of 5 PinprofessionalRaul Iloc18-May-13 6:49 
GeneralRe: My vote of 5 PinmemberMoises Barba18-May-13 11:46 
GeneralMy vote of 5 PinmemberMoises Barba14-May-13 23:40 
GeneralRe: My vote of 5 PinprofessionalRaul Iloc15-May-13 2:49 
GeneralMy vote of 5 PinmemberAlex C. Duma14-May-13 21:24 
GeneralRe: My vote of 5 PinprofessionalRaul Iloc14-May-13 21:27 
GeneralRe: My vote of 5 PinprofessionalRaul Iloc18-May-13 6:51 
GeneralMy vote of 5 Pinmemberahmed rageeb14-May-13 8:01 
GeneralRe: My vote of 5 PinprofessionalRaul Iloc14-May-13 19:49 
GeneralMy vote of 5 PinmemberMihai MOGA10-May-13 19:25 
GeneralRe: My vote of 5 PinprofessionalRaul Iloc11-May-13 7:07 
GeneralRe: My vote of 5 PinprofessionalRaul Iloc18-May-13 6:50 
GeneralMy vote of 5 PinmemberRamocip8-May-13 23:41 
GeneralRe: My vote of 5 PinprofessionalRaul Iloc9-May-13 3:00 
GeneralRe: My vote of 5 PinprofessionalRaul Iloc18-May-13 6:56 
GeneralVery good Pinprofessionaldb7uk6-May-13 13:12 
GeneralRe: Very good PinmemberRaul Iloc6-May-13 20:11 
GeneralRe: Very good PinprofessionalRaul Iloc9-May-13 21:50 
GeneralRe: Very good Pinprofessionaldb7uk10-May-13 21:49 
GeneralRe: Very good PinprofessionalRaul Iloc18-May-13 6:57 
GeneralMy vote of 5 PinmemberdgDavidGreene1-May-13 6:03 
GeneralRe: My vote of 5 PinmemberRaul Iloc2-May-13 6:06 
GeneralRe: My vote of 5 PinprofessionalRaul Iloc9-May-13 21:51 
GeneralMy vote of 5 Pinmembermanoj.jsm16-Apr-13 21:16 
GeneralRe: My vote of 5 PinmemberRaul Iloc16-Apr-13 21:21 
GeneralRe: My vote of 5 PinprofessionalRaul Iloc9-May-13 21:53 
GeneralMy vote of 5 PinmemberRamocip13-Apr-13 8:48 
GeneralRe: My vote of 5 PinmemberRaul Iloc14-Apr-13 0:28 
QuestionPage layout PinmemberDave Cross13-Apr-13 6:10 
AnswerRe: Page layout PinmemberRaul Iloc13-Apr-13 6:48 
GeneralMy vote of 5 PinmemberIonFr13-Apr-13 1:21 
GeneralRe: My vote of 5 PinmemberRaul Iloc13-Apr-13 4:29 
GeneralRe: My vote of 5 PinprofessionalRaul Iloc9-May-13 21:54 
GeneralMy vote of 5 PinmemberMember 989522812-Apr-13 5:53 
GeneralRe: My vote of 5 PinmemberRaul Iloc12-Apr-13 6:07 
GeneralRe: My vote of 5 PinprofessionalRaul Iloc9-May-13 21:54 
GeneralMy vote of 5 PinmemberDiana Lucia12-Apr-13 1:11 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.141223.1 | Last Updated 25 Oct 2013
Article Copyright 2013 by Raul Iloc
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid