BlogEngine(4)---Widget小部件

   前面的两篇文章中,我们分别介绍了BE的插件和主题机制,这一篇我们来看看BE三大特性中的最后一个:Widget。

所谓的widget,在BE中可以理解为一块特定的显示区域,在这个区域中可以用来显示文章分类信息,博主个人信息,访客信息等等一系列你可以想到的东西。在BE中,一个widget就是一个用户控件,统一放在widget目录中。当用户想添加自己的widget时只需要在widget下添加以这个widget命名的文件夹以及对应的widget控件,相当的方便。下面咱们就来通过一个简单的例子来“重现”widget的实现方法。当然,在这个例子中我只是实现了“显示”而已,额外的“编辑”,“排序”在弄懂了下面的实现后应该不难。

"重现"widget

2011-05-02_203547

   首先看一下项目图,我仍然使用的上次实现主题更换的那个项目。只不过添加了一个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的实现方式已经说完了,不知道你是否已经明白其中的流程?这是最后的效果图:

1

源码下载

http://www.vdisk.cn/down/index/7644535A9490

posted @ 2011-05-03 09:04  qianlifeng  阅读(2343)  评论(11编辑  收藏  举报