[译]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网站教程(三):动态布局和站点管理(涉及技术:AJAX、jqGrid、Controller扩展、HTML Helpers等等)

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

系列的第一篇文章“多语言网站框架”, 主要讲解如何去创建一个支持多语言的MVC网站,同时也讲解了用户认证和注册机制的实现。使用了微软的Entity Framework框架和LINQ查询技术。

系列的第二篇文章“异常管理”,提出了详细的异常管理规则并在ASP.NET MVC网站中实现异常管理,还提供一些通用的日志记录和异常管理的源代码。这些源代码不仅可以在任何ASP.NET网站中被重用(或经过比较小的改动适用),而且可以重用到任何.NET项目中。

系列的第三篇文章(即本文),实现了动态布局和站点管理,使用了AJAX、jqGrid、自定义操作结果、控制器扩展、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

这个操作结果类被用于在新浏览器窗口中打开一个文件。在本网站示例中用于在独立的浏览器窗口中打开一个PDF、JPG或PNG文件,同样也能用于打开其他类型的文件。

[译]MVC网站教程(三):动态布局和站点管理

从上面类图可知,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

这个操作结果类将图片数据流在当前视图中呈现一个图片。这个数据流可能是从数据库中加载的包含图像的数据流、也可能是从一个文件中加载的包含图像的文件流、或者其他地方传入的数据流。

[译]MVC网站教程(三):动态布局和站点管理

从上面类图中可知,这个类包含两个属性用于设置内容类型和图像流,主要功能方法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,让所有控制器能访问。

[译]MVC网站教程(三):动态布局和站点管理

从上面类图中可知,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类的静态方法成员。

[译]MVC网站教程(三):动态布局和站点管理

从上面类图中可知,这里有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视图中调用的。

ImageButton在razor视图中像下面这样使用:

@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参数的重载。

上面代码将呈现一个图片按钮以及浮动提示信息,像下面截图:

[译]MVC网站教程(三):动态布局和站点管理

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脚本触发相应的操作。

方法的最后一个命名为isReadOnly的bool类型参数,是可选参数,当这个参数设置为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);

上面代码调用将给定一系列值显示为下拉列表像下面截图:

[译]MVC网站教程(三):动态布局和站点管理

注意,上面的下拉列表仅能从给定的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);

上面代码调用将根据给定参数呈现一个复选框像下面截图:

[译]MVC网站教程(三):动态布局和站点管理

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辅助方法,传入的最后一个参数是将HTML的id属性值设置为"_leftImage"。根据传入参数将调用SiteSettingController控制器的GetHeaderImage操作从数据库中加载图片数据。

上面代码呈现的页眉图片像下面截图:

[译]MVC网站教程(三):动态布局和站点管理

在MVC4.0中引入jqGrid插件(设计技术:AJAX,JSON,JQuery,LINQ,序列化)

jqGrid是一个开源的支持AJAX的javascript控件,它提供在网络上显示和操作表格数据,并且支持通过AJAX回调函数动态的加载数据。

关于jqGrid控件的帮助文档和示例详细请看:http://www.trirand.com

我们在MVC基础网站中引入jqGrid插件。这个插件是本网站的一个重要部件,我将在下一篇文章中详细介绍该插件,但是我已将jqGrid插件的源代码包含在本篇博文对应的解决方案中。

下面是引入jqGrid插件显示的"Visitors"页面截图。(你必须使用指定帐户登录才能查看,帐户:Administrator密码:tm77dac)

[译]MVC网站教程(三):动态布局和站点管理

从上面截图中可知,页面中的grid的列有不同的类型(字符串、日期和bool),并且所有列支持排序功能。最后一个命名为"Actions"的列,使用了ImageButton自定义帮助器方法,当用户按下"Delete"图片按钮时,当前行的访问日志将从数据库中删除。

注意,grid列表的下面有两个操作按钮:"Reolad Grid"按钮和"Delete All"按钮。

动态布局和站点管理

本章节详细介绍:使用前文提到的技术(自定义操作结果、控制器扩展、HTML帮助器、AJAX和jqGrid),来实现动态布局和站点管理。

首先,我简单的描述下用于站点管理的数据实体,然后我再描述用于管理员

动态布局的站点管理页面和相关控制器类,最后我将描述动态布局的详细实现过程。

1)   数据实体

[译]MVC网站教程(三):动态布局和站点管理

上图是MVC基础站点解决方案中的部分数据实体。这里有6个实体,并且分别与数据库中的一个表关联。下面3个实体:Country,Address和User已经在《MVC网站教程(一):多语言网站框架》中介绍了。

1)   VistorLog:存储站点的访问日志条目。每个新用户访问站点将会记录以下信息:用户Id,开始日期,结束日期和过期标记。

2)   SiteSetting:存储当前站点的设置。包含如下信息:联系Id,联系Email,备用Email,页眉描述,页眉图片和站点标题。注意,这个实体存储的信息用于站点动态布局。

3)   SiteDocument:存储站点的文件数据。站点文件是由站点系统管理员动态上传的,并且与特定语言或所有语言关联。对于每个站点文件会记录下面信息:名字(显示在站点布局中),文件全名(服务器存放文件路径),语言(文件所关联的特定语言或所有语言),非公共文档标识(非公共文档只有认证用户可查看),是系统文档标识(系统文档不能被编辑和删除),是协议文档标识(协议文档是用于特定语言的站点协议),日期(上传日期)。注意,这个实体存储的信息用于站点动态布局。

2)   站点设置

站点主要设置是由站点管理员通过“Settings”页面进行维护的。

[译]MVC网站教程(三):动态布局和站点管理

从上面图片中可知,通过“Settings”页面,管理员能将设置数据存储到SiteSetting实体中。

在这个页面,我们不仅能修改联系数据,还能设置下面主要数据的动态布局:

1)   页眉图片:管理员可以选择使用默认图片,或通过上传图片设置一个新的页眉图片。

2)   页眉描述:页眉描述将显示在每个页面的页眉。

3)   页面标题:页面标题将显示在浏览器标签上。

在像上面这样修改站点设置后,新的页眉图标和页眉描述将从此刻应用到所有站点页面,如下面截图所示:

[译]MVC网站教程(三):动态布局和站点管理

SiteSettingController继承自BaseController,这个控制器管理着Settings页面的所有操作。

[译]MVC网站教程(三):动态布局和站点管理

从上面类图可知:这些操作被用于Settings页面,属性HasHeaderImage和SiteTitle以及方法GetHeaderImage被用于布局头部信息。详见源代码。

3)   站点文件

站点文档是站点管理员动态上传到服务器的文件。然后基于文档的属性设置,如用户类型(匿名用户或认证用户),当前语言(English, Română, Deutsch)等判别文档能否被用户访问并且在页脚在显示。

在当前实现中,站点文档可以是PDF、TXT、HTM、HTML、PNG或JPG文件,但可以扩展支持其他类型。

注意,如果文档是非公开的,那只有认证用户能查看这个文档。并且如果文档是特定语言的,那只有当用户使用这个语言时才能查看。

站点文档是由站点管理员通过“Site Documents”页面进行管理的。

[译]MVC网站教程(三):动态布局和站点管理

从上面截图中,我们看到站点文档数据加载到jqGrid列表控件中并且支持排序。

注意,这些列:Culture,Not Public,For Agreement和Actions使用了前面描述的自定义HTML帮助器。

站点管理员可以通过页面底部的上传控件动态添加任意多的文档。注意,每个文档的数据将被存储到数据库中,文件本身将会保存在服务器中当前站点的Content\Doc\folder文件夹中。

这个grid列表中有3个系统文档,分别命名为SiteAgreement-DE.htm,SiteAgreement-RO.htm和SiteAgreement-EN.htm,它们被用于特定语言的站点协议。这些文档是系统文档和默认站点协议文档的一部分,被用于特定语言环境,所以不能对它们进行编辑和删除。管理员可以设置其他的文档用于特定语言或所有语言的站点协议,而不再使用默认设置。

根据文档的类型(PDF,JPG和PNG文件不能被编辑)将会在最后一列显示一系列的操作。系统文档不能删除,但是可以编辑。PDF文档可以被查看和删除。TXT,HTM,HTML文档可以被编辑和删除。

比如,如果你想编辑英语语言环境下的站点协议,你必须按下触发编辑操作的图片按钮。

[译]MVC网站教程(三):动态布局和站点管理

如上面截图所示,我正在改变英语语言环境下的站点协议文档,新增一段。

然后保存更改,如果我退出并且在Home页面设置为使用英语语言环境,然后在Home页面的页脚点击“Site-Agreement”文档。文档将在站点中打开,如下图:

[译]MVC网站教程(三):动态布局和站点管理

在上面截图中,可以看到我在英语语言环境下的站点协议中所添加的一段。

注意,现在是非认证用户,所以他只能访问公共文档,并且是英语环境或全语言环境属性的文档。

现在,如果用户使用用户名Ana和密码ana登录,然后改变当前语言环境为“Romănă”,用户将可以在页脚访问更多的文档,并且这些文档和之前语言环境的文档不同,就像下图:

[译]MVC网站教程(三):动态布局和站点管理

从上图中,你能看到当前是认证用户,所以他能访问非公开文档。当前用户设置的是Romanian语言环境,所以能访问该语言或全语言属性的站点文档。在当前页面会显示联系信息。

注意,在所有情况下,在页脚列出的前两个文档都是特殊的。第一个显示联系信息(一个含有动态信息的静态页面),第二个显示的是当前语言环境下站点协议文档。

如果你点击的文档是TXT、HTML或HTML类型,将直接在站点中打开,就像上文刚提到的协议文档。但是,如果点击的文档是PDF,JPG或PNG的类型,将在一个新的浏览器窗口中打开提供给用户查看、打印或保存在本地,如下图:

[译]MVC网站教程(三):动态布局和站点管理

用户点击命名为“icon-sd”的站点文档,则与它关联的PDF文档会在一个新浏览器窗口中打开,用户可以查看,打印,发邮件和保存文档。

SiteDocumentController继承自BaseController,这个控制器类管理“Site Document”页面的所有操作和从主布局中发出与“Site Document”关联的请求。

[译]MVC网站教程(三):动态布局和站点管理

从上面类图中,你能看到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中的Details和GetFileResult操作是将文档直接在站点中打开还是在一个新浏览器窗口中打开取决于当前文档是可编辑的(TXT,HTML或HTM)还是不可编辑的(PDF,JPG或PNG)。

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自定义操作返回结果将PDF,IMG或JPG发送到浏览器并且打开一个新的浏览器窗口。

注意一下,在用于站点主布局之前,在HomeController的Index方法中已将站点文档数据从数据库中读出并且缓存。

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)   用其他方式使用解决方案中提供的文件类型(TXT,HTML,PDF,IMG,PNG)。

c)   添加CSS文档类型,并且将其用于站点布局。所有站点的CSS也能通过相同方式进行编辑。

为了实现这个,首先必须修改SiteDocument类的IsEditable和ContentType类型,然后在SiteDocuments表中添加一个或更多“system document”的CSS文件,最后将CSS样式用于站点布局。

d)   添加更多文档类型并在你的布局中使用它们。

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

作者:Raul Iloc

上一篇:“maven编码gbk的不可映射字符”解决办法


下一篇:amis学习笔记-代码分析④