MVC 基本站点:步骤 3 – 使用 AJAX、jqGrid、控制器扩展、HTML 帮助器等实现动态布局和站点管理
本文提供了在 ASP.NET MVC4.0 中使用 AJAX、jqGrid、自定义 Action Results、控制器扩展、HTML 帮助器等实现动态布局和网站管理。
MVC 基本站点
- 步骤 1 - 多语言站点骨架
- 步骤 2 - 异常管理
- 步骤 3 - 使用 AJAX、jqGrid、控制器扩展、Html 帮助器等实现动态布局和站点管理
- 步骤 4 - 使用 AJAX、JSON、jQuery、LINQ 和序列化在 MVC4.0 中集成 jqGrid
目录
- 引言
- 软件环境
- 自定义 Action Result 类和控制器扩展
- 自定义 HTML 帮助器
- 使用 AJAX、JSON、jQuery、LINQ 和序列化在 MVC4.0 中集成 jqGrid
- 动态布局和站点管理
- 如何扩展动态布局
- 运行此代码之前
- 参考文献
- 历史
引言
MVC 基本站点旨在成为一系列关于创建使用 ASP.NET MVC 的基本且可扩展网站的教程文章。
本系列的第一篇文章,名为 MVC 基本站点:步骤 1 - 多语言站点骨架,主要侧重于使用 ASP.NET MVC 创建多语言网站骨架。此外,还描述了从头开始创建的用户身份验证和注册。
第二篇文章,名为 MVC 基本站点:步骤 2 - 异常管理,详细介绍了 ASP.NET MVC 网站的异常管理规则及其实现,并提供了一些有用的基类和用于日志记录和异常管理的源代码,这些源代码不仅可以在其他 ASP.NET 网站中重用(只需很小的更改),而且通常可以在任何 .NET 项目中重用。
本系列的第三篇文章提供了动态布局和网站管理页面的实现,使用了 AJAX、jqGrid、自定义 Action Results、控制器扩展、HTML 帮助器以及其他有用的 C# 源代码和 JavaScript,这些都可以扩展和重用于其他项目。
MVC 基本站点是使用增量和迭代方法开发的,这意味着每个新步骤都会为上一步的解决方案添加更多功能,因此本文提供的下载源代码包含迄今为止(所有文章中)实现的所有功能。
网站的布局通常包括页眉、菜单和页脚。请注意,这些布局对于大多数网站来说是静态的。
动态布局意味着网站管理员只需使用 Web 应用程序管理页面提供的用户界面即可修改网站布局;管理员所做的所有更改都将保存在数据库中,从那时起,网站布局将按照网站管理员的意愿进行更改。因此,网站管理员可以随时随地通过 Web 浏览器对网站布局进行所有更改,无需手动更改或设置。
本文的第一部分描述了所使用的构建块,然后接下来的章节将继续介绍站点管理中使用的动态布局。最后一章将为您提供一些关于如何扩展动态布局的提示。
请注意,提供的所有源代码都注释良好且清晰,阅读和理解应该没有问题。此外,所使用的构建块(自定义 Action Results、控制器扩展、自定义 HTML 帮助器和其他有用的类、Razor 视图和 JavaScript)以及整个站点骨架都可以被您重用并扩展到更复杂的站点中。
软件环境
- .NET 4.0 框架
- Visual Studio 2010(或 Express 版)
- ASP.NET MVC 4.0
- SQL Server 2008 R2(或 Express Edition 版本 10.50.2500.0)
自定义 Action Result 类和控制器扩展
在本章中,我将向您介绍用于创建动态站点布局的自定义 Action Result 类和控制器扩展。
在 controllers 类中,每个 action 方法通过执行工作并返回一个 action result 来响应用户输入。action result 表示框架将代表 action 方法执行的命令。所有 actions results 类都必须派生自 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);
}
MVC 4.0 框架中有一组可用的 action results,它们都是 ActionResult
抽象类的直接或间接子类(详见 MSDN)
ContentResult
EmptyResult
FileResult
FileContentResult
FilePathResult
FileStreamResult
HttpNotFoundResult
HttpStatusCodeResult
HttpUnauthorizedResult
JavaScriptResult
JsonResult
JsonResult
RedirectResult
RedirectToRouteResult
PartialViewResult
ViewResultBase
ViewResult
为了创建动态站点布局,我使用了上面现有的一些 action result 类(来自 MVC 4.0 框架),但我也创建并使用了以下两个自定义 action result 类
OpenFileResult
ImageResult
OpenFileResult
这个 action result 类用于在新浏览器窗口中打开给定文件。在我们的例子中,它用于在单独的浏览器窗口中打开 PDF、JPG 或 PNG 文件,但也可以用于其他文件类型。
正如您从上面的类图中可以看到的,它有 3 个属性用于设置内容类型、文件名和要打开文件的虚拟路径。主要工作方法是下一个。
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 响应,浏览器将在响应中在新浏览器窗口中打开文件。
此 action result 的使用应如下例所示
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,如动态布局实现一章所示。
ImageResult
此 Action Result 类用于将给定流中的图像渲染到当前视图中。该流通常可以是包含从数据库加载的图像的内存流、包含从文件加载的图像的文件流或任何其他输入流。
正如您从上面的类图中可以看到的,这个类有两个属性,用于设置内容类型和图像流。主要工作方法是下一个。
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 响应输出流。在响应中,浏览器将在当前视图中渲染图像。
此 action result 的使用可以像上一章中的 OpenFileResult
一样完成,但在 MVC Basic Site 中,我通过控制扩展间接使用 ImageResult
action result 类。详情请参阅下一章。
控制器扩展
控制器扩展是 MVC 中可用于扩展控制器功能的机制。
在 MVC Basic 站点中,我使用 ControllerExtensions
类来允许所有控制器访问上面章节中描述的 ImageResult 自定义 action result。
如您从上面的类图中可以看到的,这是一个静态类,提供了两个名为 Image 的重载方法。请注意,这些方法具有相同的返回类型 ImageResult
(我们的自定义 action result),但它们具有不同的签名,并且都具有一个类型为 Controller 的第一个特殊参数(详见下文),该参数标识当前控制器。
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);
}
上述方法应用于从给定字节数组渲染给定内容类型的图像。字节数组可以是image field从数据库读取的图像字节,也可以是WCF(Windows Communication Foundation)服务接收的图像数据。
控制器扩展的使用示例如下。
public ImageResult GetHeaderImage(int id)
{
SiteSetting shopSetting = _db.SiteSettings.First();
//
return this.Image(shopSetting.HeaderImage, "image/jpeg");
}
此代码首先从数据库加载页眉图像数据,然后使用字节数组在网站布局上渲染图像。请注意,该方法被调用时就像它属于当前控制器一样,并且方法签名中的第一个参数(Controller 类型)被间接使用。
自定义 HTML 帮助器
我用来实现动态布局的另一个主要构建块是自定义 HTML 帮助器。所有自定义帮助器都作为静态方法包含在我的 RenderHelpers
类中。
正如您从上面的类图中可以看到的,有一组四种不同的自定义 HTML 帮助器,并且它们都有重载方法,用于在不同的参数和不同的上下文中调用帮助器:从 Razor 视图或从控制器源代码。
ImageButton
有一组 4 个同名方法实现了 ImageButton
自定义 HTML 帮助器。这些是我创建的最简单的 HTML 帮助器,用于根据给定参数渲染图像按钮,并将图像按钮与指定控制器的操作关联起来。
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 视图中调用,它具有最大数量的参数,并且是执行工作的方法。它根据给定参数渲染图像按钮,并将其与指定控制器的操作(应该是与当前控制器不同的第二个控制器)关联起来。
如您从上面的源代码中可以看到,此方法根据给定参数在锚标记中创建一个图像标记。给定的 HTML 属性仅用于图像标记,而链接属性仅用于锚标记。
请注意,此方法被下面描述的所有其他重载 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();
}
上述代码根据给定参数渲染一个图像按钮,并将其与第二个控制器的一个操作关联。此方法设计用于从当前控制器使用,但它使用第二个控制器的操作。
从 razor 视图中使用 ImageButon
HTML 帮助器应如下例所示
@Html.ImageButton(Resource.ViewTip,
"~/Content/view.png",
"GetFileResult",
new { id = host.ID },
new { style = "border:0px;" },
new { target = "blank_" })
请注意,“@Html
”语句用于调用自定义 HTML 帮助器,MVC 框架将识别 RenderHelpers
类的正确方法,并自动将当前控制器发送给它们。请注意,最后一个参数将 HTML 属性“target
”设置为值“blank_
”,因此当用户点击此图像按钮时,由 GetFileResult
操作返回的具有指定 ID 的文件(例如,服务器上的 PDF 文件)将在新窗口中由浏览器打开。请注意,按钮的工具提示(第一个参数)是从资源文件中读取的,并且是多语言的。
RenderHelpers.ImageButton(this,
Resource.ViewTip,
"~/Content/view.png",
"GetFileResult",
new { id = host.ID },
new { style = "border:0px;" },
new { target = "blank_" })
请注意,在上面的源代码中,动作 GetFileResult
属于第二个控制器 SiteDocumentControler
,它与当前控制器不同。在这种情况下,我还额外使用了一个新参数,即作为第一个参数发送的当前控制器。
上述源代码渲染的图像按钮带工具提示,如下图所示
EnumDropDownList
有一组 2 个同名方法实现 EnumDropDownList
自定义 HTML 帮助器,它们为泛型 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
,是可选的,当设置为 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();
}
上述代码根据给定参数渲染一个下拉列表,并将其与控制器的一个动作关联。此方法设计用于从当前控制器源代码中使用。
从控制器源代码中使用 EnumDropDownList
HTML 帮助器应如下例所示
Culture = RenderHelpers.EnumDropDownList(this,
"dropDown",
"SiteDocument/SetCultureInfo",
host.Culture != null ? (SiteCultures)host.Culture : SiteCultures.All,
host.IsSystemDoc == null ? false : true);
上述源代码从一组值中调用,渲染的下拉列表如下图所示
请注意,在上面的源代码中,只给出了枚举中当前选定的值,并且根据其类型,泛型 TEnum
将被当前使用的枚举替换。此外,action 值为 “SiteDocument/SetCultureInfo
”;因此它必须是一个包含控制器名称后跟 action 名称的 URL。请注意,action SetCultureInfo
必须具有如下签名:
public ActionResult SetCultureInfo(string value, string id)
当用户更改下拉列表的选定项时,将调用此操作,并将接收当前选定项的值和 ID 作为参数(在我们的例子中是枚举文本及其数字)。
CustomCheckBox
有一组两个同名方法实现 CustomCheckBox
自定义 HTML 帮助器,它们为其他给定参数渲染一个复选框。
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();
}
上述代码根据给定参数渲染一个复选框,并将其与控制器的一个动作关联。此方法设计用于从当前控制器源代码中使用。
从控制器源代码中使用 CustomCheckBox
HTML 帮助器应如下例所示
IsNotPublic = RenderHelpers.CustomCheckBox(this,
"checkBox1",
"SiteDocument/SetIsNotPublic",
host.IsNotPublic != null && host.IsNotPublic == true ? "1" : "0",
host.IsSystemDoc == null ? false : true);
上述源代码从一组值中调用,渲染的下拉列表如下图所示
ImageFromStream
有一组四个同名方法实现了 ImageFromStream
自定义 HTML 帮助器。
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 视图中调用,它具有最大数量的参数,并且是执行工作的方法。它根据流和给定的其他参数渲染图像。图像数据由指定控制器的动作和给定的图像 ID 读取(在我们的例子中,数据是从数据库加载的,但图像也可以来自服务器文件夹)。
如您从上面的源代码中可以看到,此方法根据给定参数创建图像标签。用于获取图像数据的 URL 是使用给定控制器操作构建的。给定的 HTML 属性也仅用于图像标签。
请注意,此方法被下面描述的所有其他重载 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, action, null, 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();
}
上述代码根据给定参数渲染一个图像按钮,并将其与当前控制器的一个动作关联以加载图像数据。此方法设计用于从 Razor 视图中使用。
从 Razor 视图中使用 ImageFromStream
HTML 帮助器应如下例所示
@Html.ImageFromStream("Home",
"SiteSetting",
"GetHeaderImage",
7,
new { id = "_leftImage" })
请注意,“@Html
”语句用于调用自定义 HTML 帮助器,MVC 框架将识别 RenderHelpers
类的正确方法,并自动将当前控制器发送给它们。请注意,最后一个参数将 HTML 属性“id
”设置为值“leftImage
”,并且有一些 CSS 样式与此 ID 相关联。图像通过 SiteSettingController
的“GetHeaderImage
”动作读取,在我们的例子中,图像数据是从数据库读取的。
上述源代码渲染的页面页眉图像如下图所示
使用 AJAX、JSON、jQuery、LINQ 和序列化在 MVC4.0 中集成 jqGrid
jqGrid 是一个开源的、支持 AJAX 的 JavaScript 控件,提供在 Web 上表示和操作表格数据的解决方案,并能通过 AJAX 回调动态加载数据。
jqGrid 的文档和示例可以在以下网站找到:http://www.trirand.com
jqGrid 在我们的 MVC Basic Site 中的集成是通过 AJAX、JSON、jQuery、JavaScript、Linq 和序列化完成的。由于这种集成是一个很大的部分,我将在下一篇 MVC Basic Site 文章中详细介绍它,但源代码已经包含在当前解决方案中。
下面是管理员区域“访客”页面中(您必须使用用户 Administrator 和密码 tm77dac 登录)此集成的结果截图。
如您从上面的截图可以看到,页面中的网格有不同类型的列(字符串、日期时间和布尔值),所有列都启用了排序。最后一列,名为“Actions”,使用了我的 ImageButton
自定义 HTML 帮助器(如上所述);当用户点击“Delete”图像按钮(在动作列中)时,相关的访客日志条目将从数据库中删除。
请注意,网格底部有一个分页控制器,还有两个操作按钮:“重新加载网格”按钮(图像按钮)和“全部删除”按钮 - 用于删除当前过滤器下的所有访客日志历史记录。
动态布局和站点管理
本章详细介绍了使用上述所有构建块(自定义 Action Results、控制器扩展、HTML 帮助器、AJAX 和 jqGrid)实现动态布局和网站管理。
首先,我将简要介绍用于站点数据的数据实体,然后继续介绍用于管理动态布局的站点管理页面及其控制器类,最后我将详细介绍动态布局的实现。
数据实体
上图是 MVC Basic Site 解决方案使用的数据实体图。有 6 个实体,每个实体都与一个名称相似的 SQL 数据库表相关联。上面图中的 Country、Address 和 User 这 3 个实体已在 MVC Basic Site – 步骤 1 文章中描述。
有三个用于站点管理和动态布局的新实体
- VisitorLog:存储站点访客日志条目;对于每个新用户站点使用情况,存储以下信息:UserID、开始日期、结束日期和是否超时标志。
- SiteSetting:存储当前站点设置;包括以下信息:联系人ID(与地址表中的联系地址链接)、联系人邮箱、支持邮箱、页眉描述、页眉图像和站点标题。请注意,此实体存储用于动态站点布局的信息。
- SiteDocument:用于存储站点文档数据。站点文档是系统管理员可以动态上传并与特定文化信息或所有文化信息相关联的文件。对于每个站点文档,存储以下信息:名称(显示在站点布局中)、文件完整名称(文档文件在服务器上的路径)、文化(文档可供所有文化访问或仅供一种指定文化访问)、非公共标志(非公共文档只能由已认证用户查看)、系统文档标志(系统文档不能编辑或删除)、用于协议标志(文档用于特定文化的站点协议)、日期(上传日期)。请注意,此实体存储用于动态站点布局的信息。
站点设置
网站主要设置由网站管理员通过“设置”页面进行管理。
如您从上图可以看到,通过“设置”页面,管理员可以设置上面描述的 SiteSetting 实体中存储的数据。
因此,通过这种方式,可以编辑联系人数据和支持邮箱,还可以编辑以下动态布局中使用的主要数据
- 页眉图像:管理员有两种可能性:使用默认图像,或通过选择和上传所需的图像来指定新的页眉图像;
- 页眉描述:将在网站布局中显示的页眉描述;
- 页面标题:将在浏览器中显示为页面标题前缀的页面标题,例如:“基本站点 – 设置”。
用户修改网站设置后,如上图所示,结果将是新的页眉图像和描述将从该点开始用于所有网站布局,如下图所示。
请注意,在上面的截图中,网站管理布局使用了新的页眉图像和页眉描述。但如果您注销,您会发现主(用户)布局也将使用新的网站设置。
SiteSeetingController
派生自 BaseController
,它是管理“设置”页面中操作的控制器类。
您可以在上面的类图中看到“设置”页面中使用的操作,以及属性 HasHeaderImage
和 SiteTitle
,以及在布局页眉中用于读取站点设置信息的方法 GetHeaderImage()
。您可以在源代码中查看更多详细信息。
站点文档
站点文档是指与文件关联的文档,由站点管理员动态上传到服务器,然后根据文档属性、用户类型(匿名用户或已认证用户)、当前使用的文化(英语、罗马尼亚语、德语),用户可以从主布局页脚访问和查看该文档。
在当前实现中,文档文件可以是 PDF、TXT、HTM、HTML、PNG 或 JPG 文件,但可以添加其他类型。
请注意,如果文档是非公开的,这意味着只有经过身份验证的用户才能查看该文档,如果文档是特定文化的,则用户只有在使用该文化时才能看到该文档。
站点文档由站点管理员通过“文档”页面进行管理。
在上面的截图中,您可以看到 jqGrid 中的站点文档数据,其列可排序。
请注意,上述网格列中的单元格:Culture、Not Public、For Agreement 和 Actions 均使用我上面描述的自定义 HTML 帮助器进行渲染。
站点管理员可以使用页面底部的上传控件动态添加任意数量的文档。请注意,对于每个新文档,其数据将存储在数据库中,文件将保存在服务器上的站点 Content\Doc\ 文件夹中。
网格中有 3 个名为 SiteAgreement-DE.htm、SiteAgreement-RO.htm 和 SiteAgrement-EN.htm 的“系统”文档,它们用于关联文化的站点协议。这些文档是系统的一部分,是指定文化使用的默认站点协议文档,因此不能删除,但可以编辑。管理员可以为一种文化或所有文化设置其他站点协议文档,从那时起,该文档将用于站点协议,而不是默认文档。
根据文档类型(PDF、JPG 和 PNG 文件不可编辑),最后一列中为每个文档提供了一组操作。系统文档不能删除,但可以编辑。PDF 文档可以删除或查看,TXT、HTM 和 HTML 文档可以编辑或删除。
例如,如果您想编辑英文文化的站点协议,您需要点击关联的“编辑”图像按钮,协议文件将在“编辑”页面中打开进行编辑。
在上面的截图中,您可以看到我通过添加一个新的段落来更改了“英文”文化的网站协议。
保存更改后,如果我注销并从主页使用“英文”文化,然后从主页页脚点击“网站协议”文档,该文档将在网站中打开,如下图所示。
在上面的截图中,您可以看到新添加的段落现在已在站点协议文档中。
请注意,用户未经过身份验证,因此只能访问公共文档以及“所有”文化和“英语”文化的文档。
现在,如果用户使用用户名 Ana 和密码 ana 登录,然后将当前文化更改为“罗马尼亚语”,则用户将(在页面页脚中)访问更多文档,并且某些文档与第一种情况不同,如下图所示。
在上图中,您可以看到当前用户已通过身份验证,因此可以访问非公开文档,并且他/她正在使用罗马尼亚语文化,因此可以访问在罗马尼亚语文化中使用的站点文档。在当前页面中显示了“联系”信息(来自站点设置)。
请注意,在所有情况下,主布局页脚的前两个文档都是特殊的。第一个显示联系信息(一个带有动态内容的静态页面),第二个是当前使用的文化的当前站点协议文档。
如果用户点击类型为 TXT、HTML 或 HTM 的文档,该文档将直接在网站中打开,就像我们已经看到协议文档的情况一样;但如果文档是 PDF、JPG 或 PNG,该文档将在另一个浏览器窗口中打开,以便用户可以查看、打印和/或将其保存到他/她的计算机上,如下图所示。
用户刚刚点击了名为“icon-sd”的网站文档,关联的 PDF 文档在新浏览器窗口中打开,现在用户可以查看、打印、发送电子邮件和/或保存该文档。
SiteDocumentController
派生自 BaseController
,它是管理管理员“站点文档”页面中的操作,以及用户请求时从主布局调用的与“站点文档”相关的操作的控制器类。
在上面的类图中,您可以看到 SiteDocumentController
的所有方法。更多详细信息请参阅源代码和下一章。
动态布局实现
在本章中,我将向您介绍动态布局的实现细节。
在当前版本中,MVC Basic Site 使用以下两种布局
- _AdminLayout.cshtml -仅用于管理员页面;
- _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>
请注意,我正在使用上面描述的自定义 HTML 帮助器 ImageFromStream
来渲染由 SiteSettingControler
返回的数据的图像。
站点文档,可以由系统管理员动态设置,在主布局页脚中动态渲染,如下面的 Razor 代码所示。
@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 列动态呈现。第一行中的第一个位置不用于站点文档,而是用于打开“联系人”页面。第二个位置始终用于当前站点协议,因为当从数据库读取站点文档数据时,当前用户和当前文化的当前站点协议始终添加到第二个位置。
请注意,根据文档是否为可编辑文档(TXT、HTML 或 HTM)或不可编辑文档(PDF、JPG 或 PNG),使用 SiteDocumentController
的 Details
或 GetFileResult
操作来直接在站点中打开文档或在新浏览器页面中打开文档。
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);
}
上述源代码将在用户请求时(点击页脚中的文档)调用,并从服务器读取可编辑文档的数据,然后将文档数据显示在网站的“详细信息”页面中。请注意,Request.IsLocal
属性用于从 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;
}
上述源代码将在用户请求时(点击页脚中的文档)调用,并从服务器读取不可编辑文档的数据,然后将使用 OpenFileResult
(我上面描述的自定义 Action Result)将 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();
}
因为站点文档取决于当前用户和当前选定的文化,所以在上面的源代码中,首先将获取当前站点会话,然后我们读取当前用户并使用当前选定的文化从数据库中读取站点文档数据(详见下文)。返回的结果随后缓存到会话缓存中,然后用于主布局。
在下面的源代码中使用了 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();
}</sitedocument>
在应用程序逻辑层,上面的源代码使用 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 读取当前给定文化的商店协议文档。它查找是否存在管理员定义的用于替代默认协议的站点协议文档;如果存在此类文档,则将使用该文档,否则将使用给定文化信息的默认站点协议文档(系统文档)。
如何扩展动态布局
本文提供的动态布局实现可以通过以下方式进行扩展
- 在数据库
Settings
表中添加更多设置,然后扩展Settings
管理页面以允许编辑它们,最后根据它们在“联系我们”页面和/或网站布局中显示更多信息。 - 以不同于所提供解决方案的方式使用现有文档类型(TXT、HTML、PDF、IMG、PNG)。例如,可以在您的网站布局菜单中动态添加打开网站文档的链接。通过这种方式,网站管理员可以动态地向您的网站添加新的 HTML 页面,例如,用于显示新闻。
- 添加 CSS 类型的文档,并在站点布局中使用它们,这样站点的 CSS 就可以通过这种方式动态修改。
为此,您必须首先修改
SiteDocument
类中的以下属性:IsEditable
和ContentType
,然后您应该在SiteDocuments
表中添加一个或多个 CSS 文件的“系统文档”,最后在您的站点布局中使用 CSS。 - 添加更多文档类型,并按照本文指示和/或您希望的方式在布局中使用它们。
为此,您应该按照上面第 3 点所示的步骤进行操作。
运行此代码之前
在运行此代码之前,您应该执行以下步骤:
- 以管理员身份运行 CreateEventLogEntry 应用程序(CreateEventLogEntry 应用程序源代码作为我们解决方案的一部分提供)以在事件日志中创建一个新条目;
- 在您的 SQL Server(或 SQL Express)中创建一个名为 MvcBasicSite 的数据库,然后将提供的数据库 MvcBasicSiteDatabase.bak 恢复到其中。
- 根据您在步骤 2 中的设置,修改 MvcBasicSite Web 应用程序的 Web.config 文件中的连接字符串。
如果这篇文章和提供的源代码对您有帮助,您可以投票;如果您有问题,我在这里帮助您!
参考
- MVC 基本站点:步骤 1 - 多语言站点骨架
- MVC 基本站点:步骤 2 - 异常管理
- Microsoft Visual Studio 2010 Express
- ASP.NET MVC 4.0
- SQL Server 2008 R2 Express
历史
- 2013 年 4 月 11 日:版本 1.0.0.1 - 草稿版本。
- 2013 年 4 月 12 日:版本 1.0.0.2 - 发布版本。
- 2013 年 4 月 19 日:版本 1.0.1.1 - 添加新章节。
- 2013 年 4 月 28 日:版本 1.0.1.2 - 添加新信息。
- 2013 年 5 月 18 日:版本 1.0.1.2 - 更新 MVC 基本步骤。