前面的两篇文章中,我们分别介绍了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的实现方式已经说完了,不知道你是否已经明白其中的流程?这是最后的效果图: