[譯]MVC網站教程(三):動態布局和站點管理


 

目錄

1.   介紹

2.   軟件環境

3.   在運行示例代碼之前(源代碼 + 示例登陸帳號)

4.   自定義操作結果和控制器擴展

1)   OpenFileResult

2)   ImageResult

5.   控制器擴展

6.   自定義HTML幫助器

1)   ImageButton

2)   EnumDropDownList

3)   CustomCheckBox

4)   ImageFromStream

7.   在MVC4.0中引入jqGrid插件(涉及技術:AJAX,JSON,JQuery,LINQ,序列化

8.   動態布局和站點管理

1)   數據實體

2)   站點設置

3)   站點文件

4)   實現動態布局

9.   如何擴展動態布局

 

介紹

MVC網站教程”系列的目的是教你如何使用 ASP.NET MVC 創建一個基本的、可擴展的網站。

1)   MVC網站教程(一):多語言網站框架

2)   MVC網站教程(二):異常管理

3)   MVC網站教程(三):動態布局和站點管理(涉及技術:AJAXjqGridController擴展、HTML Helpers等等)

4)   MVC網站教程(四):MVC4網站中集成jqGrid表格插件(涉及技術:AJAX,JSON,jQuery,LINQ和序列化)

 

系列的第一篇文章“多語言網站框架”, 主要講解如何去創建一個支持多語言的MVC網站,同時也講解了用戶認證和注冊機制的實現。使用了微軟的Entity Framework框架和LINQ查詢技術。

    系列的第二篇文章異常管理”,提出了詳細的異常管理規則並在ASP.NET MVC網站中實現異常管理,還提供一些通用的日志記錄和異常管理的源代碼。這些源代碼不僅可以在任何ASP.NET網站中被重用(或經過比較小的改動適用),而且可以重用到任何.NET項目中。

    系列的第三篇文章(即本文),實現了動態布局和站點管理,使用了AJAXjqGrid、自定義操作結果、控制器擴展、HTML幫助器,還使用了一些通用的C#源代碼和javascript腳本,這些都能被擴展和被重用到其它項目中。

       MVC網站教程”系列的示例網站是采用增量式和迭代式軟件過程開發的,這意味着系列中每一篇博文會在前一篇的解決方案中添加更多的功能,所以本文提供的示例下載只包含系列目前為止所介紹的功能。

    網站的布局通常包括標題頁眉、菜單和頁腳。注意,這些布局對大多數網站都是不變的。

    動態布局,意思是網站管理員能通過網站應用程序提供的管理頁面修改網站的布局。管理員的所有修改數據都會保存在數據庫中,並且從此以后網站的布局就會按照網站管理員的設置呈現。因此網站管理員能通過web瀏覽器在任何時候、任何地點改變網站的布局。

    本博文主要包含三個部分。第一部分描述網站中使用的構建塊;第二部分演示使用網站管理頁面來動態布局;第三部分就如何擴展動態布局給出一些提示。

    網站的構建塊(如:自定義操作結果、控制器擴展、自定義HTML幫助器以及一些其他公用類,Razor視圖和Javascript腳本等等)以及整個站點框架你可以重用和擴展成更復雜的網站。

 

軟件環境

1.   .NET 4.0 Framework

2.   Visual Studio 2010 (or Express edition)

3.   ASP.NET MVC 4.0

4.   SQL Server 2008 R2 (or Express Edition version 10.50.2500.0)

 

在運行示例代碼之前

在運行示例代碼之前,你應該做下面事情:

1.   首先使用“管理員身份”運行CreateEventLogEntry控制台項目程序產生的exe,用來在事件日志中創建“MVC Basic”事件源。(EventLog在寫日志時會創建指定名稱的類別默認為“應用程序”的事件源。但是ASP.NET網站沒有足夠的權限來創建事件源,需要本地桌面應用程序)

2.   在你的SQL Server服務器中創建一個名為MvcBasicSite的數據庫,然后用我提供的MvcBasicSiteDatabase.bak文件進行數據庫還原。

3.   修改MVC應用程序示例的Web.config配置文件中的鏈接字符串。

 

示例帳號

1)   管理員帳戶:Administrator   密碼:tm77dac

2)   普通帳戶: Ana              密碼:ana

 

本博文示例下載:

1)   動態布局和站點管理MVC4—示例源代碼.zip

2)   動態布局和站點管理—數據庫bak.zip

 

自定義操作結果和控制器擴展

    在本節中,我將介紹自定義操作結果和控制器擴展,用於創建站點的動態布局。

    在控制器類中,響應用戶輸入的每個操作方法執行完工作后返回一個操作結果。操作結果代表MVC框架執行完一個操作指令。所有操作結果類必須繼承自ActionResult抽象類。這個抽象類包含下面成員:

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);
}

這里有一系列MVC4.0框架提供的操作結果,它們都直接或間接繼承自ActionResult抽象類:

1)   ContentResult

2)   EmptyResult

3)   FileResult

4)   FileContentResult

5)   FilePathResult

6)   FileStreamResult

7)   HttpStatusCodeResult

8)   HttpUnauthorizedResult

9)   JavaScriptResult

10)JsonResult

11)RedirectResult

12)RedirectToRouteResult

13)PartialViewResult

14)ViewResultBase

15)ViewResult

為了創建動態布局,我使用了一些上面已經存在的操作結果類,但我也創建下面兩個自定義操作結果類:

1)   OpenFileResult

2)   ImageResult

 

1.   OpenFileResult

這個操作結果類被用於在新瀏覽器窗口中打開一個文件。在本網站示例中用於在獨立的瀏覽器窗口中打開一個PDFJPGPNG文件,同樣也能用於打開其他類型的文件。

image

    從上面類圖可知,OpenFileResult包含3個屬性,被用於設置內容類型、文件名字和文件存放的虛擬路徑。主要功能方法ExecuteResult()如下:

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();
}

從上面代碼可知,這個方法首先設置內容類型,然后將文件傳送到HTTP響應中,收到響應結果的瀏覽器將在新窗口中打開一個文件。

應該像下面例子一樣使用OpenFileResult

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;
}

如果是PDF文檔,那么將在瀏覽器新窗口中打開PDF

 

2.   ImageResult

這個操作結果類將圖片數據流在當前視圖中呈現一個圖片。這個數據流可能是從數據庫中加載的包含圖像的數據流、也可能是從一個文件中加載的包含圖像的文件流、或者其他地方傳入的數據流。

image

 

   從上面類圖中可知,這個類包含兩個屬性用於設置內容類型和圖像流,主要功能方法ExecuteResult()如下:

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);
    }
}

    從上面源代碼中可知,這個方法首先設置內容類型,然后讀取圖像並存到數據流中,最后將數據流以字節流的方式寫到HTTP響應輸出流中。在接收到響應的瀏覽器上會將圖片呈現到當前視圖中。

    ImageResult操作結果的使用方法和上文提到的OpenFileResult相似,但是在MVC示例網站中我是直接作為控制器擴展(ControllerExtensions類)來使用的,詳細見下文。

 

控制器擴展

控制器擴展,可在MVC中用於擴展控制器功能。

    MVC網站示例中,我使用ControllerExtensions類來提供創建ImageResult自定義操作結果的API,讓所有控制器能訪問。

image

    從上面類圖中可知,ControllerExtensions是一個靜態類,它提供一個擁有兩個命名為Image的重載方法。 注意,這兩個重載方法都返回ImageResult,但它們簽名不同,如下:

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

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);
}

第一個,用於呈現一個指定內容類型和圖片數據流的圖片。

第二個,用於呈現一個指定內容類型和圖片字節數組的圖片,這個字節數組可以是從數據庫、圖片文件中讀取,也可以是從WCF(Windows Communication Foundation)服務中接受到的圖片數據。

控制器擴展可以像下面這樣使用:

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

    這段代碼,首先從數據庫中加載圖片數據,然后使用字節數組來呈現一個圖片。

 

自定義HTML幫助器

    自定義HTML幫助器也是實現動態布局的重要構建塊,所有自定義幫助器都作為RenderHelper類的靜態方法成員。

image

    從上面類圖中可知,這里有4個不同的自定義HTML幫助器,並且它們都有重載方法,可以在razor視圖中、Controller代碼中使用不同的參數進行調用。

 

1.   ImageButton

ImageButton自定義輔助幫助器有4個重載,下面這個是重載通過指定的參數呈現圖像按鈕,這個圖像按鈕會與指定的控制器操作進行關聯。

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));
}

    這個方法是在razor視圖中調用的,它用來根據指定參數呈現圖片按鈕,並且將其與指定控制器的操作關聯起來。

    從上面源代碼中可知,這個方法使用指定參數創建一個<a>標簽並且里面嵌套一個<img>標簽。htmlAttributes屬性僅用於<img>標簽,linkAttributes屬性僅用於<a>標簽。

    注意,上面這個方法被所有其他ImageButton重載方法使用。

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();
}

上面代碼會根據提供的參數呈現一個圖片按鈕,並且與當前控制器的操作進行關聯。這個方法是在當前控制器中調用的。

 

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);
}

上面代碼會根據提供的參數呈現一個圖片按鈕,並且與當前控制器的操作進行關聯。這個方法是在razor視圖中調用的。

 

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();
}

    上面代碼會根據提供的參數呈現一個圖片按鈕,並且與controllerName參數指定的控制器的操作進行關聯。這個方法是在razor視圖中調用的。

    ImageButtonrazor視圖中像下面這樣使用:

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

    注意,"@Html"語法是用於調用HtmlHelper類提供的方法,以及在RenderHelpers類中為HtmlHelper類定義的擴展方法。注意:

1)   沒有使用帶controllerName參數的重載,所以默認使用當前控制器。

2)   第一個參數傳遞的文本是從資源文件中讀取的,以便支持多語言。

3)   最后一個參數設置HTML屬性target值為"blank_",所以當用戶單擊這個圖片按鈕時,GetFileResult操作會根據指定的Id獲取文件並在新的瀏覽器窗口中呈現。

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

    注意上面代碼中,GetFileResult操作是在SiteDocumentControler控制器中聲明,並非當前控制器,所以在本例中我們使用了另一個含有controllerName參數的重載。

       上面代碼將呈現一個圖片按鈕以及浮動提示信息,像下面截圖:

    image

 

2.   EnumDropDownList

       EnumDropDownList自定義幫助器有2個重載方法,能根據泛型參數TEnum以及其他輸入參數呈現一個下拉列表。

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());
    }
}

        從上面源代碼中可知,這個方法使用給定參數創建一個下拉列表。列表中你能選擇指定的值,並且當下拉列表中當前選擇項改變時會使用javascript腳本觸發相應的操作。

        方法的最后一個命名為isReadOnlybool類型參數,是可選參數,當這個參數設置為true的時候將呈現一個只讀的下拉列表。

        注意,這個方法被下面EnumDropDownList重載方法調用。

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();
}

        上面代碼使用給定參數呈現一個下拉列表,並關聯上指定Controller的操作。這個方法被設計為在當前控制代碼中調用。

        在控制器代碼中應該像下面示例這樣調用EnumDropDownList輔助幫助器方法。

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

    上面代碼調用將給定一系列值顯示為下拉列表像下面截圖:

image

   

        注意,上面的下拉列表僅能從給定的Enum枚舉參數中選擇值。泛型參數TEnum將被替換為當前使用的枚舉。action參數為“SiteDocument/SetCultureInfo”必須是URL格式,包含Controller名,接着是action名字。SetCultureInfo操作必須有一個如下簽名的重載:

       public ActionResult SetCultureInfo(string value, string id)

        當用戶改變下拉列表值時,將觸發SetCultureInfo操作。

3.   CustomCheckBox

       CustomCheckBox自定義幫助器有2個重載方法,它使用給定參數呈現一個復選框。

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));
}   

       這個方法設計為在razor視圖中調用。

       如上面代碼所示,這個方法呈現一個復選框並且通過javascript腳本關聯一個給定的控制器操作。倒數第二個參數命名為isReadOnly,當設置為true時,將呈現一個只讀的復選框。

       注意,這個方法被下面CustomCheckBox重載方法調用。

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();
}

       上面代碼根據給定參數呈現一個復選框,並關聯上指定Controller的操作。這個方法被設計為在當前控制代碼中調用。

       在控制器代碼中應該像下面示例這樣調用CustomCheckBox輔助幫助器方法:

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

    上面代碼調用將根據給定參數呈現一個復選框像下面截圖:

image

   

4.   ImageFromStream

    ImageFromStream自定義幫助器有4個重載方法:

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());
    }
}

    這個方法被設計為在razor視圖中調用,這是最多參數的一個方法重載。這個方法根據給定的參數獲取數據流最后呈現一個圖片。這個圖片數據可以從數據庫中加載也可以從服務器文件夾中獲取。

    如上面代碼所示,這個方法根據給定的參數創建了一個圖片標簽。URL指定的控制器操作用於獲取圖片數據。給定的HTML屬性對象同樣用於生成Image標簽。

    注意,這個方法被下面ImageFromStream重載方法調用。

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();
}

    上面代碼根據給定的參數呈現一個圖片並且關聯當前控制器的操作用於加載圖片數據。這個方法被設計為在控制器中調用。

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

    上面代碼根據給定的參數呈現一個圖片並且關聯當前控制器的操作用於加載圖片數據。這個方法被設計為在razor視圖中調用。

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();
}

    上面代碼根據給定的參數呈現一個圖片並且關聯controllerName參數指定控制器的操作用於加載圖片數據。這個方法被設計為在控制器中調用。

    razor視圖代碼中應該像下面示例這樣調用ImageFromStream輔助幫助器方法:

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

       注意,這里的"@Html"語句用於調用在RenderHelpers類中我們自定義的HTML輔助方法,傳入的最后一個參數是將HTMLid屬性值設置為"_leftImage"。根據傳入參數將調用SiteSettingController控制器的GetHeaderImage操作從數據庫中加載圖片數據。

    上面代碼呈現的頁眉圖片像下面截圖:

image

 

MVC4.0中引入jqGrid插件(設計技術:AJAXJSONJQueryLINQ,序列化)

    jqGrid是一個開源的支持AJAXjavascript控件,它提供在網絡上顯示和操作表格數據,並且支持通過AJAX回調函數動態的加載數據。

    關於jqGrid控件的幫助文檔和示例詳細請看:http://www.trirand.com

    我們在MVC基礎網站中引入jqGrid插件。這個插件是本網站的一個重要部件,我將在下一篇文章中詳細介紹該插件,但是我已將jqGrid插件的源代碼包含在本篇博文對應的解決方案中。

   下面是引入jqGrid插件顯示的"Visitors"頁面截圖。(你必須使用指定帳戶登錄才能查看,帳戶:Administrator密碼:tm77dac

image

    從上面截圖中可知,頁面中的grid的列有不同的類型(字符串、日期和bool),並且所有列支持排序功能。最后一個命名為"Actions"的列,使用了ImageButton自定義幫助器方法,當用戶按下"Delete"圖片按鈕時,當前行的訪問日志將從數據庫中刪除。

    注意,grid列表的下面有兩個操作按鈕:"Reolad Grid"按鈕和"Delete All"按鈕。

 

動態布局和站點管理

    本章節詳細介紹:使用前文提到的技術(自定義操作結果、控制器擴展、HTML幫助器、AJAXjqGrid),來實現動態布局和站點管理。

    首先,我簡單的描述下用於站點管理的數據實體,然后我再描述用於管理員

動態布局的站點管理頁面和相關控制器類,最后我將描述動態布局的詳細實現過程。

1)   數據實體

image

       上圖是MVC基礎站點解決方案中的部分數據實體。這里有6個實體,並且分別與數據庫中的一個表關聯。下面3個實體:CountryAddressUser已經在《MVC網站教程(一):多語言網站框架》中介紹了。

1)   VistorLog:存儲站點的訪問日志條目。每個新用戶訪問站點將會記錄以下信息:用戶Id,開始日期,結束日期和過期標記。

2)   SiteSetting:存儲當前站點的設置。包含如下信息:聯系Id,聯系Email,備用Email,頁眉描述,頁眉圖片和站點標題。注意,這個實體存儲的信息用於站點動態布局。

3)   SiteDocument:存儲站點的文件數據。站點文件是由站點系統管理員動態上傳的,並且與特定語言或所有語言關聯。對於每個站點文件會記錄下面信息:名字(顯示在站點布局中),文件全名(服務器存放文件路徑),語言(文件所關聯的特定語言或所有語言),非公共文檔標識(非公共文檔只有認證用戶可查看),是系統文檔標識(系統文檔不能被編輯和刪除),是協議文檔標識(協議文檔是用於特定語言的站點協議),日期(上傳日期)。注意,這個實體存儲的信息用於站點動態布局。

2)   站點設置

    站點主要設置是由站點管理員通過“Settings”頁面進行維護的。

image

       從上面圖片中可知,通過“Settings”頁面,管理員能將設置數據存儲到SiteSetting實體中。

       在這個頁面,我們不僅能修改聯系數據,還能設置下面主要數據的動態布局:

1)   頁眉圖片:管理員可以選擇使用默認圖片,或通過上傳圖片設置一個新的頁眉圖片。

2)   頁眉描述:頁眉描述將顯示在每個頁面的頁眉。

3)   頁面標題:頁面標題將顯示在瀏覽器標簽上。

       在像上面這樣修改站點設置后,新的頁眉圖標和頁眉描述將從此刻應用到所有站點頁面,如下面截圖所示:

image

 

       SiteSettingController繼承自BaseController,這個控制器管理着Settings頁面的所有操作。

image

 

       從上面類圖可知:這些操作被用於Settings頁面,屬性HasHeaderImageSiteTitle以及方法GetHeaderImage被用於布局頭部信息。詳見源代碼。

3)   站點文件

    站點文檔是站點管理員動態上傳到服務器的文件。然后基於文檔的屬性設置,如用戶類型(匿名用戶或認證用戶),當前語言(English, Română, Deutsch)等判別文檔能否被用戶訪問並且在頁腳在顯示。

    在當前實現中,站點文檔可以是PDFTXTHTMHTMLPNGJPG文件,但可以擴展支持其他類型。

    注意,如果文檔是非公開的,那只有認證用戶能查看這個文檔。並且如果文檔是特定語言的,那只有當用戶使用這個語言時才能查看。

    站點文檔是由站點管理員通過“Site Documents”頁面進行管理的。

image

 

       從上面截圖中,我們看到站點文檔數據加載到jqGrid列表控件中並且支持排序。

       注意,這些列:CultureNot PublicFor AgreementActions使用了前面描述的自定義HTML幫助器。

       站點管理員可以通過頁面底部的上傳控件動態添加任意多的文檔。注意,每個文檔的數據將被存儲到數據庫中,文件本身將會保存在服務器中當前站點的Content\Doc\folder文件夾中。

       這個grid列表中有3個系統文檔,分別命名為SiteAgreement-DE.htmSiteAgreement-RO.htmSiteAgreement-EN.htm,它們被用於特定語言的站點協議。這些文檔是系統文檔和默認站點協議文檔的一部分,被用於特定語言環境,所以不能對它們進行編輯和刪除。管理員可以設置其他的文檔用於特定語言或所有語言的站點協議,而不再使用默認設置。

       根據文檔的類型(PDFJPGPNG文件不能被編輯)將會在最后一列顯示一系列的操作。系統文檔不能刪除,但是可以編輯。PDF文檔可以被查看和刪除。TXTHTMHTML文檔可以被編輯和刪除。

       比如,如果你想編輯英語語言環境下的站點協議,你必須按下觸發編輯操作的圖片按鈕。

image

 

       如上面截圖所示,我正在改變英語語言環境下的站點協議文檔,新增一段。

       然后保存更改,如果我退出並且在Home頁面設置為使用英語語言環境,然后在Home頁面的頁腳點擊“Site-Agreement”文檔。文檔將在站點中打開,如下圖:

image

 

       在上面截圖中,可以看到我在英語語言環境下的站點協議中所添加的一段。

       注意,現在是非認證用戶,所以他只能訪問公共文檔,並且是英語環境或全語言環境屬性的文檔。

       現在,如果用戶使用用戶名Ana和密碼ana登錄,然后改變當前語言環境為“Romănă,用戶將可以在頁腳訪問更多的文檔,並且這些文檔和之前語言環境的文檔不同,就像下圖:

image

 

       從上圖中,你能看到當前是認證用戶,所以他能訪問非公開文檔。當前用戶設置的是Romanian語言環境,所以能訪問該語言或全語言屬性的站點文檔。在當前頁面會顯示聯系信息。

       注意,在所有情況下,在頁腳列出的前兩個文檔都是特殊的。第一個顯示聯系信息(一個含有動態信息的靜態頁面),第二個顯示的是當前語言環境下站點協議文檔。

       如果你點擊的文檔是TXTHTMLHTML類型,將直接在站點中打開,就像上文剛提到的協議文檔。但是,如果點擊的文檔是PDFJPGPNG的類型,將在一個新的瀏覽器窗口中打開提供給用戶查看、打印或保存在本地,如下圖:

image

 

       用戶點擊命名為“icon-sd”的站點文檔,則與它關聯的PDF文檔會在一個新瀏覽器窗口中打開,用戶可以查看,打印,發郵件和保存文檔。

       SiteDocumentController繼承自BaseController,這個控制器類管理“Site Document”頁面的所有操作和從主布局中發出與“Site Document”關聯的請求。

image

 

       從上面類圖中,你能看到SiteDocumentController的所有操作方法。更詳細請看源代碼。

4)   實現動態布局

在本章節,我將向你介紹動態布局的詳細實現。

在當前版本,MVC基礎網站使用下面兩個排版:

a)   _AdminLayout.cshtml:只用於管理員頁面。

b)   _Layout.cshtml:用戶頁面的主要排版。

       這兩個排版都通過“_Header.cshtml”局部頁面實現相同的頁眉,但是它們的菜單和頁腳不同。

        系統管理員可以動態設置站點文檔,文檔將動態呈現在主布局的頁腳,就像下面razor代碼。

<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>

       注意,我使用了在前文描述的ImageFromStream自定義HTML幫助器,使用SiteSettingController返回的數據呈現一個圖片。

@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>
    }
}	

       注意,從上面razor代碼可知,站點文檔被動態呈現為4列。表格的第一行第一個位置不是用於站點文檔,而是用來打開“Contact”頁面。第二個位置總是用於放置當前站點協議文檔.

    注意,SiteDocumentController中的DetailsGetFileResult操作是將文檔直接在站點中打開還是在一個新瀏覽器窗口中打開取決於當前文檔是可編輯的(TXTHTMLHTM)還是不可編輯的(PDFJPGPNG)。

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);
}

       上面代碼將在用戶單擊頁腳的文檔並將從服務器讀取一個可編輯的文檔時執行,然后將文檔數據顯示在站點的“Details”頁面。注意,Request.IsLocal屬性用於判斷請求是否來自本地計算機。

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;
}

        上面代碼將在用戶單擊頁腳的文檔並將從服務器讀取一個不可編輯的文檔時執行,然后使用OpenFileResult自定義操作返回結果將PDFIMGJPG發送到瀏覽器並且打開一個新的瀏覽器窗口。

       注意一下,在用於站點主布局之前,在HomeControllerIndex方法中已將站點文檔數據從數據庫中讀出並且緩存。

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();
}

       因為站點文檔依賴於當前登錄用戶和當前選擇的語言環境,在源代碼中我首先獲取當前站點會話數據,然后讀取當前用戶數據和根據當前選擇的語言環境從數據庫中讀取站點文檔。返回的站點文檔會被緩存在session中,然后被用於主布局。

       在源代碼中還是用了下面SiteCultures枚舉:

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

       上面源代碼定義了站點語言環境以及其對應的整數值。注意,在SiteDocument實體中Culture屬性的整數值就與此枚舉對應。

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();
}

       在應用程序邏輯層,GetSiteDocumentsForUser方法根據給定的參數,使用LINQ從數據庫中讀取當前用戶用權限的商店文檔以及讀取站點協議文檔插入返回數組的第一個位置。

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();
}

       上面代碼使用LINQ讀取適合語言環境下的商店協議文檔。如果存在站點管理員定義的協議文檔代替默認文檔,那么直接使用。否則特定語言環境的系統定義的默認協議文檔將被使用

 

如何擴展動態布局

本篇博文解決方案提供的站點動態布局可通過下面方式進行擴展:

a)   Settings表中添加更多設置,然后擴展“Settings”管理員頁面允許編輯新增的設置,最后將這些信息在Contact頁面或站點布局中顯示。

b)   用其他方式使用解決方案中提供的文件類型(TXTHTMLPDFIMGPNG)。

c)   添加CSS文檔類型,並且將其用於站點布局。所有站點的CSS也能通過相同方式進行編輯。

    為了實現這個,首先必須修改SiteDocument類的IsEditableContentType類型,然后在SiteDocuments表中添加一個或更多“system document”的CSS文件,最后將CSS樣式用於站點布局。

d)   添加更多文檔類型並在你的布局中使用它們。

 

 

原文:http://www.codeproject.com/Articles/576286/MVC-Basic-Site-Step-Dynamic-Layouts-and-Site-Adm

作者:Raul Iloc

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM