BlogEngine(4)---Widget小部件
前面的两篇文章中,我们分别介绍了BE的插件和主题机制,这一篇我们来看看BE三大特性中的最后一个:Widget。
所谓的widget,在BE中可以理解为一块特定的显示区域,在这个区域中可以用来显示文章分类信息,博主个人信息,访客信息等等一系列你可以想到的东西。在BE中,一个widget就是一个用户控件,统一放在widget目录中。当用户想添加自己的widget时只需要在widget下添加以这个widget命名的文件夹以及对应的widget控件,相当的方便。下面咱们就来通过一个简单的例子来“重现”widget的实现方法。当然,在这个例子中我只是实现了“显示”而已,额外的“编辑”,“排序”在弄懂了下面的实现后应该不难。
首先看一下项目图,我仍然使用的上次实现主题更换的那个项目。只不过添加了一个widgets文件夹,并在其中放置了Search和TextBox两个widget,具体的widget.ascx中的内容我们后面再看。
重点看下面三个用户控件。
WidgetBase.ascx:这个用户控件时所有widget的基类,所有的widget都要继承这个用户控件。它定义了所有widget的一些通用的属性,比如名字,是否可编辑,是否显示标题等等。
WidgetContainer.ascx:这个用户控件可以看成是对widget的一层包装,所有的widget最后并不是直接显示到页面中的,而是要经过这个控件的包装确定统一的显示外观后再显示到页面中。这样做的好处显而易见,用户在前台能够看到一个具有统一界面与操作体验的widget。
WidgetZone.ascx:所有的用户控件最后不可能散落的显示在页面的各个地方,肯定有一个专门的地方(容器)在盛放这些控件。而这个WidgetZone就是用来盛放这些控件的了。
大体介绍了几个必要的控件的作用。我们从具体的一个widget入手(比如说Search),来看看到底widget是怎样被加入到页面中并显示出来的。下面是search的widget.ascx中的代码:
public partial class Widget : WidgetBase { public override bool IsEditable { get { return true; } } public override string Name { get { return "搜索"; } } public override void LoadWidget() { //var settings = this.GetSettings(); //if (!settings.ContainsKey("content")) //{ // return; //} string content = "<input type='text' id='key' /><input type='submit' value='search' id='btnSubmit'/>"; LiteralControl text = new LiteralControl { Text = content }; this.Controls.Add(text); } }
根据前面讲到的,这个widget必须继承WidgetBase,以便让每个widget都有统一的属性。在这个widget中没有自己的方法,都是通过override来重写了父类中的方法或者属性。那就来看看WidgetBase中的内容:
public abstract partial class WidgetBase : System.Web.UI.UserControl { /// <summary> /// Gets a value indicating whether the header is visible. This only takes effect if the widgets isn't editable. /// </summary> /// <value><c>true</c> if the header is visible; otherwise, <c>false</c>.</value> public virtual bool DisplayHeader { get { return true; } } /// <summary> /// Gets a value indicating whether or not the widget can be edited. /// <remarks> /// The only way a widget can be editable is by adding a edit.ascx file to the widget folder. /// </remarks> /// </summary> public abstract bool IsEditable { get; } /// <summary> /// Gets the name. It must be exactly the same as the folder that contains the widget. /// </summary> public abstract string Name { get; } /// <summary> /// Gets or sets a value indicating whether [show title]. /// </summary> /// <value><c>true</c> if [show title]; otherwise, <c>false</c>.</value> public bool ShowTitle { get; set; } /// <summary> /// Gets or sets the title of the widget. It is mandatory for all widgets to set the Title. /// </summary> /// <value>The title of the widget.</value> public string Title { get; set; } /// <summary> /// Gets or sets the widget ID. /// </summary> /// <value>The widget ID.</value> public Guid WidgetId { get; set; } /// <summary> /// Gets or sets the name of the containing WidgetZone /// </summary> public string Zone { get; set; } /// <summary> /// GetSettings会根据WidgetID从储存介质中获得相应的配置信息(也就是内容信息)并存储在Cache中 /// </summary> public StringDictionary GetSettings() { //MOCK var cacheId = string.Format("be_widget_{0}", this.WidgetId); if (this.Cache[cacheId] == null) { StringDictionary s = new StringDictionary(); s.Add("content", "<a href='#' >text href</a>"); // var ws = new WidgetSettings(this.WidgetId.ToString()); this.Cache[cacheId] = s; } return (StringDictionary)this.Cache[cacheId]; } /// <summary> /// 这个方法在用户自定义的widget中被重写 /// </summary> public abstract void LoadWidget(); protected override void Render(HtmlTextWriter writer) { if (string.IsNullOrEmpty(this.Name)) { throw new NullReferenceException("Name must be set on a widget"); } base.Render(writer); } }
前面的那一大堆用英文注释的属性是我直接从BE中拿过来的,就是定义了一些widget的共有的特性。值得注意的有两个方法:1.GetSetting,这个方法用于从存储介质(数据库,xml)中获得这个widget的一些配置信息,相当于给每个widget提供了一个自由存储的功能。2.loadWidget,这个方法是一个抽象方法,在子类中实现。好像看到这里我们并没有看到这个方法是怎样被调用的,先不着急。我们接着往下看 :)
现在widget都一切准备就绪了,就差其他人来将它加到特定的 显示区域显示了。这个任务交给widgetZone来完成。下面看看widgetZone的实现:
public abstract partial class WidgetZone : System.Web.UI.UserControl { //用于存放所有的WidgetZone及其对应的子widget信息,无论WidgetZone有几个,这个只有一个 private static readonly Dictionary<string, XmlDocument> XmlDocumentByZone = new Dictionary<string, XmlDocument>(); private string zoneName = "be_WIDGET_ZONE"; /// <summary> /// 区域的名字(标志) /// </summary> public string ZoneName { get { return zoneName; } set { zoneName = value; } } /// <summary> /// 这个zone包含的子widgetxml信息 /// </summary> private XmlDocument XmlDocument { get { return XmlDocumentByZone.ContainsKey(ZoneName) ? XmlDocumentByZone[ZoneName] : null; } } protected override void OnInit(EventArgs e) { //从存储介质中获得这个widgetZone所包含的widget信息 if (XmlDocument == null) { //Mock data string mockXml = "<widgets>" + "<widget id=\"d9ada63d-3462-4c72-908e-9d35f0acce40\" title=\"TextBox\" showTitle=\"True\">TextBox</widget> " + "<widget id=\"19baa5f6-49d4-4828-8f7f-018535c35f94\" title=\"Administration\" showTitle=\"True\">Administration</widget> " + "<widget id=\"d81c5ae3-e57e-4374-a539-5cdee45e639f\" title=\"Search\" showTitle=\"True\">Search</widget> " + "<widget id=\"77142800-6dff-4016-99ca-69b5c5ebac93\" title=\"Tag cloud\" showTitle=\"True\">Tag cloud</widget>" + "<widget id=\"4ce68ae7-c0c8-4bf8-b50f-a67b582b0d2e\" title=\"RecentPosts\" showTitle=\"True\">RecentPosts</widget>" + "</widgets>"; XmlDocument xmlDocument = new XmlDocument(); xmlDocument.LoadXml(mockXml); XmlDocumentByZone[ZoneName] = xmlDocument; } base.OnInit(e); } protected override void OnLoad(EventArgs e) { //将取出的每个widget控件写入 var zone = this.XmlDocument.SelectNodes("//widget"); if (zone == null) { return; } //// This is for compatibility with older themes that do not have a WidgetContainer control. //var widgetContainerExists = WidgetContainer.DoesThemeWidgetContainerExist(); //var widgetContainerVirtualPath = WidgetContainer.GetThemeWidgetContainerVirtualPath(); foreach (XmlNode widget in zone) { var fileName = string.Format("{0}widgets/{1}/widget.ascx", Utils.RelativeWebRoot, widget.InnerText); try { //加载特定的控件,控件类型为WidgetBase(因为每个控件都继承了WidgetBase) var control = (WidgetBase)Page.LoadControl(fileName); if (widget.Attributes != null) { //从读取的xml属性中将值复制给control属性 control.WidgetId = new Guid(widget.Attributes["id"].InnerText); control.Title = widget.Attributes["title"].InnerText; control.ShowTitle = control.IsEditable ? bool.Parse(widget.Attributes["showTitle"].InnerText) : control.DisplayHeader; } control.ID = control.WidgetId.ToString().Replace("-", string.Empty); control.Zone = zoneName; control.LoadWidget(); //将此控件包装到widgetContainer里面,这样每个control都有一个统一的外观(修改,删除按钮在这里统一) var widgetContainer = WidgetContainer.GetWidgetContainer(control); //将包装好的widget加入这个zone中 Controls.Add(widgetContainer); } catch (Exception ex) { //找不到则不加载 } } base.OnLoad(e); } protected override void Render(System.Web.UI.HtmlTextWriter writer) { writer.Write("<div id=\"widgetzone_{0}\" class=\"widgetzone\">", this.zoneName); base.Render(writer); writer.Write("</div>"); //如果没有权限修改widget,则不输出管理按钮 //if (!Security.IsAuthorizedTo(Rights.ManageWidgets)) //{ // return; //} //var selectorId = string.Format("widgetselector_{0}", this.zoneName); //writer.Write("<select id=\"{0}\" class=\"widgetselector\">", selectorId); //var di = new DirectoryInfo(this.Page.Server.MapPath(string.Format("{0}widgets", Utils.RelativeWebRoot))); //foreach (var dir in di.GetDirectories().Where(dir => File.Exists(Path.Combine(dir.FullName, "widget.ascx")))) //{ // writer.Write("<option value=\"{0}\">{1}</option>", dir.Name, dir.Name); //} //writer.Write("</select> "); //writer.Write( // "<input type=\"button\" value=\"添加部件\" onclick=\"BlogEngine.widgetAdmin.addWidget(BlogEngine.$('{0}').value, '{1}')\" />", //by Spoony // selectorId, // this.zoneName); //writer.Write("<div class=\"clear\" id=\"clear\"> </div>"); } }
一些属性我们就不啰嗦了,懂点看一下OnInit和OnLoad方法。在Oninit方法中会从存储介质中加载xml格式的需要加载的widget信息,里面记录了这个widgetZone需要加载哪些widget,注意到XmlDocumentByZone这个属性是个静态的方法,也就是说如果有多个widgetZone,那么这个键值对里面将会有多个值。接着看onload方法,先将前面读取到的xml中的所有widget标签解析出来,这样就能得到具体的widget的信息。然后通过Page.LoadControl来加载widgets文件夹下面对应的widget,然后根据读取的xml信息,给这个从loadControl中加载的widget设置一些基本的信息(因为继承了WidgetBase,所以这里的设值就可以统一了)。设置完之后调用control.LoadWidget(); 来执行用户在loadwidget方法中的代码。最后在通过widgetContainer将此widget包装一下加入这个zone,具体怎么包装的我们继续来看widgetContainer就知道了。
public partial class WidgetContainer : System.Web.UI.UserControl { /// <summary> /// 要包装的widget /// </summary> public WidgetBase Widget { get; set; } /// <summary> /// 获得操作按钮的html代码 /// </summary> protected string AdminLinks { get { //根据用户是否登录,判断是否显示操作按钮(删除,修改等) //if (Security.IsAuthorizedTo(Rights.ManageWidgets)) //{ if (this.Widget != null) { var sb = new StringBuilder(); var widgetId = this.Widget.WidgetId; sb.AppendFormat("<a class=\"delete\" href=\"#\" onclick=\"BlogEngine.widgetAdmin.removeWidget('{0}');return false\" title=\"{1} widget\"><span class=\"widgetImg imgDelete\"> </span></a>", widgetId, "delete"); sb.AppendFormat("<a class=\"edit\" href=\"#\" onclick=\"BlogEngine.widgetAdmin.editWidget('{0}', '{1}');return false\" title=\"{2} widget\"><span class=\"widgetImg imgEdit\"> </span>", this.Widget.Name, widgetId, "edit"); sb.AppendFormat("<a class=\"move\" href=\"#\" onclick=\"BlogEngine.widgetAdmin.initiateMoveWidget('{0}');return false\" title=\"{1} widget\"><span class=\"widgetImg imgMove\"> </span></a>", widgetId, "move"); return sb.ToString(); } //} return String.Empty; } } /// <summary> /// Raises the <see cref="E:System.Web.UI.Control.Load"/> event. /// </summary> /// <param name="e">The <see cref="T:System.EventArgs"/> object that contains the event data.</param> protected override void OnLoad(EventArgs e) { base.OnLoad(e); ProcessLoad(); } private bool _processedLoad; /// <summary> /// Manually run the Initialization process. /// </summary> public void ProcessLoad() { if (_processedLoad) { return; } // phWidgetBody is the control that the Widget control // gets added to. var widgetBody = this.FindControl("phWidgetBody"); if (widgetBody != null) { widgetBody.Controls.Add(this.Widget); } else { var warn = new LiteralControl { Text = "无法在当前主题模板的部件容器中找到 ID 为 \"phWidgetBody\" 的控件."//by Spoony }; this.Controls.Add(warn); } _processedLoad = true; } /// <summary> /// Raises the <see cref="E:System.Web.UI.Control.PreRender"/> event. /// </summary> /// <param name="e">An <see cref="T:System.EventArgs"/> object that contains the event data.</param> protected override void OnPreRender(EventArgs e) { base.OnPreRender(e); // Hide the container if the Widget is null or also not visible. this.Visible = (this.Widget != null) && this.Widget.Visible; } /// <summary> /// 从主题中得到widgetContainer的位置 /// </summary> /// <returns></returns> public static string GetThemeWidgetContainerVirtualPath() { return string.Format("~/themes/{0}/WidgetContainer.ascx", "stardard" /*为了演示方便,这里直接读取默认的主题*/); } /// <summary> /// 得到特定主题下的widgetContainer的物理位置 /// </summary> /// <returns></returns> public static string GetThemeWidgetContainerFilePath() { return HostingEnvironment.MapPath(GetThemeWidgetContainerVirtualPath()); } /// <summary> /// 是否存在widgetContainer文件 /// </summary> /// <returns></returns> public static bool DoesThemeWidgetContainerExist() { // This is for compatibility with older themes that do not have a WidgetContainer control. return File.Exists(GetThemeWidgetContainerFilePath()); } /// <summary> /// 加载widgetContainer,用于包装widget,如果当前主题文件没有提供widgtContainer.ascx,则使用默认的容器 /// </summary> /// <param name="widgetControl"></param> /// <param name="widgetContainerExists"></param> /// <param name="widgetContainerVirtualPath"></param> /// <returns></returns> private static WidgetContainer GetWidgetContainer( WidgetBase widgetControl, bool widgetContainerExists, string widgetContainerVirtualPath) { //如果主题提供了用于包装的widgetContainer,则读取。否则返回某人的WidgetContainer WidgetContainer widgetContainer = widgetContainerExists ? (WidgetContainer)widgetControl.Page.LoadControl(widgetContainerVirtualPath) : new DefaultWidgetContainer(); widgetContainer.ID = "widgetContainer" + widgetControl.ID; widgetContainer.Widget = widgetControl; return widgetContainer; } /// <summary> /// 加载widgetContainer,用于包装widget,如果当前主题文件没有提供widgtContainer.ascx,则使用默认的容器 /// </summary> public static WidgetContainer GetWidgetContainer( WidgetBase widgetControl) { return GetWidgetContainer(widgetControl, DoesThemeWidgetContainerExist(), GetThemeWidgetContainerVirtualPath()); } }
重点来看GetWidgetContainer这个方法。他有三个参数,第一个就是我们要包装的widget对象,第二标明了主题中是否提供了包装样式,如果没有那么就使用默认的包装样式,第三个参数是主体的虚拟路径,用来从主题文件中加载包装样式文件。接着,程序通过判断widgetContainerExists 来判断到底应该使用哪种包装样式,然后将传进来的widget对象赋值给这个包装对象的widget属性,供render的时候使用。具体的render方法并不在这个widgetContainer中。而是在默认提供的包装样式控件和主题提供的样式中,我们看一下某人提供的包装容器:
internal sealed class DefaultWidgetContainer : WidgetContainer { /// <summary> /// The widgetBody instance needed by all WidgetContainers. /// </summary> private readonly System.Web.UI.WebControls.PlaceHolder widgetBody = new System.Web.UI.WebControls.PlaceHolder { ID = "phWidgetBody" }; /// <summary> /// Initializes a new instance of the <see cref="DefaultWidgetContainer"/> class. /// </summary> internal DefaultWidgetContainer() { this.Controls.Add(this.widgetBody); } /// <summary> /// Sends server control content to a provided <see cref="T:System.Web.UI.HtmlTextWriter"/> object, which writes the content to be rendered on the client. /// </summary> /// <param name="writer">The <see cref="T:System.Web.UI.HtmlTextWriter"/> object that receives the server control content.</param> protected override void Render(HtmlTextWriter writer) { if (this.Widget == null) { throw new NullReferenceException("WidgetContainer requires its Widget property be set to a valid WidgetBase derived control"); } var widgetName = this.Widget.Name; var widgetId = this.Widget.WidgetId; if (string.IsNullOrEmpty(this.Widget.Name)) { throw new NullReferenceException("Name must be set on a widget"); } var sb = new StringBuilder(); sb.AppendFormat("<div class=\"widget {0}\" id=\"widget{1}\">", widgetName.Replace(" ", string.Empty).ToLowerInvariant(), widgetId); sb.Append(this.AdminLinks); if (this.Widget.ShowTitle) { sb.AppendFormat("<h4>{0}</h4>", this.Widget.Title); } else { sb.Append("<br />"); } sb.Append("<div class=\"content\">"); writer.Write(sb.ToString()); base.Render(writer); writer.Write("</div>"); writer.Write("</div>"); } }
在默认的提供的包装容器中,首先声明了一个placeholder用来放置widget,不然在WidgetContainer下的processLoad方法中会报错。主要还是看render方法,在这里就是具体怎样显示这个widget的外表了,比如标题应该显示在哪里,内容显示在哪里等等布局。这样就给所有的widget提供统一的样式了。
好了,到这里widget的实现方式已经说完了,不知道你是否已经明白其中的流程?这是最后的效果图: