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中的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | 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中的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | 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的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | 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就知道了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | 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中。而是在默认提供的包装样式控件和主题提供的样式中,我们看一下某人提供的包装容器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | 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的实现方式已经说完了,不知道你是否已经明白其中的流程?这是最后的效果图:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· .NET周刊【3月第1期 2025-03-02】
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器