我曾经一直梦想拥有象windows窗体开发中一样易使用的WebMenu、WebTreeNavigatorPanel、WebDropDownTree等树结构的Web控件,当理解asp.net后我发现实现他们将不再是一个梦,目前我已经初步开发出了这些树状控件,在今后有时间的日子愿把开发心得与大家分享。
一、树状结构框架
首先针对视图状态我开发了两个基类ViewStatePartBase和ViewStatePartCollectionBase,今后所有Menu、TreeNavigator、DropDownTree的Item、ItemColleciton均从他们继承。为了便于理解这两个基类的作用,首先我们先来开发一个简单的控件WinPop来说明基类的用法。
WinPop组件主要用在网站的首页,当用户请求主页时,它会根据控件中的Items属性(弹出窗口链接地址,弹出窗口属性等等)来弹出多个窗口。其开发原型如下:
以下是ViewStatePartBase的源代码
using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.Design;
using System.Drawing.Design;
using System.Drawing;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Collections;
namespace Keyss.WebControls
{
public abstract class ViewStatePartBase:IStateManager
{
viewstate#region viewstate
protected bool _isTrackingViewState;
protected StateBag _viewState;
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[EditorBrowsable(EditorBrowsableState.Never)]
[Browsable(false)]
public virtual bool IsTrackingViewState
{
get { return _isTrackingViewState; }
}
[EditorBrowsable(EditorBrowsableState.Never)]
public virtual void LoadViewState(object savedState)
{
if(savedState != null)
{
((IStateManager)ViewState).LoadViewState(savedState);
}
}
[EditorBrowsable(EditorBrowsableState.Never)]
public virtual object SaveViewState()
{
object savedState = null;
if(!IsEmpty)
savedState = ((IStateManager)_viewState).SaveViewState();
return savedState;
}
[EditorBrowsable(EditorBrowsableState.Never)]
public virtual void TrackViewState()
{
_isTrackingViewState = true;
if(_viewState!=null)
((IStateManager)_viewState).TrackViewState();
}
#endregion
helper#region helper
[
Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)
]
protected virtual StateBag ViewState
{
get
{
if(_viewState == null)
{
_viewState = new StateBag(false);
if(_isTrackingViewState)
{
((IStateManager)_viewState).TrackViewState();
}
}
return _viewState;
}
}
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
public virtual bool IsEmpty
{
get { return (_viewState == null||ViewState.Count==0); }
}
[EditorBrowsable(EditorBrowsableState.Never)]
public virtual void SetDirty()
{
if(!IsEmpty)
{
ICollection Keys = _viewState.Keys;
foreach(string key in Keys)
{
_viewState.SetItemDirty(key, true);
}
}
}
[EditorBrowsable(EditorBrowsableState.Never)]
public virtual bool IsSet(string key)
{
if((!IsEmpty)&&(_viewState[key]!=null))
return true;
return false;
}
[EditorBrowsable(EditorBrowsableState.Never)]
public virtual void RemoveItem(string key)
{
if((_viewState[key]!=null))
this._viewState.Remove(key);
}
[EditorBrowsable(EditorBrowsableState.Never)]
public virtual void Reset()
{
_viewState = null;
}
[EditorBrowsable(EditorBrowsableState.Never)]
protected virtual void CopyFrom(ViewStatePartBase s)
{
if((s!=null)&&(!s.IsEmpty))
{
foreach(string key in s.ViewState.Keys)
{
ViewState.Add(key,s.ViewState[key]);
}
}
}
[EditorBrowsable(EditorBrowsableState.Never)]
protected virtual void MergeWith(ViewStatePartBase s)
{
if((s!=null)&&(!s.IsEmpty))
{
foreach(string key in s.ViewState.Keys)
{
if(!IsSet(key))
ViewState.Add(key,s.ViewState[key]);
}
}
}
#endregion
}
}
从上面的源代码可以看到ViewStatePartBase实现了IStateManager接口,除了标准的四个接口函数以外还实现了几个工具函数用来帮助对视图状态中保存的条目进行操作。其中Reset、MergeWith和CopyFrom主要针对自定义的样式类(如MenuItemStyle等等)操作。
以下是ViewStatePartBaseCollection的源代码
using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.Design;
using System.Drawing.Design;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.ComponentModel.Design;
namespace Keyss.WebControls
{
[
PersistenceMode(PersistenceMode.InnerProperty),
]
public abstract class ViewStatePartCollectionBase:System.Collections.CollectionBase,IStateManager
{
viewstate#region viewstate
protected bool _isTrackingViewState;
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[EditorBrowsable(EditorBrowsableState.Never)]
[Browsable(false)]
public virtual bool IsTrackingViewState
{
get { return _isTrackingViewState; }
}
[EditorBrowsable(EditorBrowsableState.Never)]
public virtual void LoadViewState(object savedState)
{
if(savedState != null)
{
Pair savedPair = (Pair)savedState;
ArrayList collectionState = (ArrayList)savedPair.First;
ArrayList collectionIndex = (ArrayList)savedPair.Second;
for(int i = 0;i < collectionState.Count; i++)
{
int index = (int)collectionIndex[i];
if(index < Count)
((ViewStatePartBase)InnerList[index]).LoadViewState(collectionState[i]);
else
{
ViewStatePartBase item = this.NewItem();
item.LoadViewState(collectionState[i]);
AddItem(item);
}
}
}
}
protected abstract ViewStatePartBase NewItem();
protected virtual int AddItem(ViewStatePartBase item)
{
if(item!=null)
{
if(this._isTrackingViewState)
{
item.TrackViewState();
item.SetDirty();
}
this.InnerList.Add(item);
return this.InnerList.Count - 1;
}
else
{
throw new ArgumentNullException("item","item can't be null!");
}
}
[EditorBrowsable(EditorBrowsableState.Never)]
public virtual object SaveViewState()
{
if(this.Count > 0)
{
ArrayList collectionState;
ArrayList collectionIndex;
collectionState = new ArrayList();
collectionIndex = new ArrayList();
for(int i=0;i< Count;i++)
{
object tmpState = ((ViewStatePartBase)InnerList[i]).SaveViewState();
if(tmpState!=null)
{
collectionState.Add(tmpState);
collectionIndex.Add(i);
}
}
if(collectionState.Count > 0)
return new Pair(collectionState,collectionIndex);
}
return null;
}
[EditorBrowsable(EditorBrowsableState.Never)]
public virtual void TrackViewState()
{
_isTrackingViewState = true;
for(int i=0;i<Count;i++)
{
((ViewStatePartBase)InnerList[i]).TrackViewState();
}
}
#endregion
helper function#region helper function
public virtual bool IsEmpty
{
get {return Count==0;}
}
public virtual void SetDirty()
{
for(int i = 0;i< Count ;i++)
((ViewStatePartBase)InnerList[i]).SetDirty();
}
#endregion
}
}
由代码可以看到,ViewStatePartCollectionBase从CollectionBase继承,这保证在设计器中会为此属性提供集合编辑器,而PersistenceMode(PersistenceMode.InnerProperty)属性则保证在页面序列化时会把其中的内容作为内部子属性来实现, 如下面Items属性所示:
而除了实现自身直接属性的视图状态管理以外还实现了其中包含的Items的状态管理,另外为了今后可以从用程序载入Item并支持视图状态管理,还实现了AddItem方法。为了简单,目前集合中只支持添加操作,并不支持插入、删除等操作,因为如果支持这些操作则还要考虑保存所有的Items中的视图状态情况比较烦锁,而一般来讲通常这些组件都是在第一次请求时装载,在回传情况下通过视图状态恢复,所以一般情况下已经够用。
下面是WinPopItem的实现
using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.Design;
using System.Drawing.Design;
using System.Drawing;
using System.ComponentModel;
using System.ComponentModel.Design;
namespace Keyss.WebControls
{
[TypeConverter(typeof(ExpandableObjectConverter))]
public class WinPopItem:ViewStatePartBase
{
const#region const
public const string TargetUrlKey = "B";
public const string TargetFrameKey = "C";
public const string TargetFeaturesKey = "D";
public const string ReplaceKey = "E";
#endregion
field#region field
[
NotifyParentProperty(true),
Bindable(true),
Category("Appearance"),
DefaultValue("")
]
public string TargetUrl
{
get
{
object o = ViewState[TargetUrlKey];
return (o == null)?string.Empty:(string)o;
}
set
{
ViewState[TargetUrlKey] = value;
}
}
[
NotifyParentProperty(true),
Bindable(true),
Category("Appearance"),
DefaultValue("")
]
public string TargetFrame
{
get
{
object o = ViewState[TargetFrameKey];
return (o == null)?string.Empty:(string)o;
}
set
{
ViewState[TargetFrameKey] = value;
}
}
[
NotifyParentProperty(true),
Bindable(true),
Category("Appearance"),
DefaultValue("")
]
public string TargetFeatures
{
get
{
object o = ViewState[TargetFeaturesKey];
return (o == null)?string.Empty:(string)o;
}
set
{
ViewState[TargetFeaturesKey] = value;
}
}
[
DefaultValue(true),
NotifyParentProperty(true),
]
public bool Replace
{
get
{
object o = ViewState[ReplaceKey];
return (o == null)?true:(bool)o;
}
set
{
ViewState[ReplaceKey] = value;
}
}
#endregion
copy merge#region copy merge
[EditorBrowsable(EditorBrowsableState.Never)]
public virtual void CopyFrom(WinPopItem s)
{
base.CopyFrom(s);
}
[EditorBrowsable(EditorBrowsableState.Never)]
public virtual void MergeWith(WinPopItem s)
{
base.MergeWith(s);
}
#endregion
prerender#region prerender
[EditorBrowsable(EditorBrowsableState.Never)]
public virtual string GetPreRenderJScript()
{
// window.open(url, target, features, replace);
return string.Format("window.open({0},{1},{2},{3})",new object[]
{
RenderHelper.StrToJavaPara(this.TargetUrl),
RenderHelper.StrToJavaPara(this.TargetFrame),
RenderHelper.StrToJavaPara(this.TargetFeatures),
RenderHelper.BoolToJavaPara(this.Replace)
}) + ";\n\r";
}
#endregion
load from table#region load from table
public virtual void LoadFromDataRow(System.Data.DataRow row)
{
System.Data.DataTable table = row.Table;
if(table.Columns.Contains("TargetURL"))
{
this.TargetUrl = (string)row["TargetURL"];
}
if(table.Columns.Contains("TargetFrame"))
{
this.TargetFrame = (string)row["TargetFrame"];
}
if(table.Columns.Contains("TargetFeatures"))
{
this.TargetFeatures = (string)row["TargetFeatures"];
}
if(table.Columns.Contains("Replace"))
{
this.Replace = (bool)row["Replace"];
}
}
#endregion
}
}
WinPopItem的实现比较简单,只是在基类上增加了一些自定义属性,并且增加一个GetPreRenderJScript() 函数用来用属性生成前台的jscript程序字符串。而LoadFromDataRow则用来从一个tablerow中加载属性。
以下是WinPopItemCollection的源代码
using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.Design;
using System.Drawing.Design;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.ComponentModel.Design;
namespace Keyss.WebControls
{
[
PersistenceMode(PersistenceMode.InnerProperty),
]
public class WinPopItemCollection:ViewStatePartCollectionBase
{
public int Add(WinPopItem item)
{
return base.AddItem(item);
}
public WinPopItem this[int index]
{
get
{
if(index >= Count || index < 0)
{
return null;
}
return (WinPopItem)this.InnerList[index];
}
}
protected override ViewStatePartBase NewItem()
{
return new WinPopItem();
}
}
}
WinPopItemCollection的实现了集合的Add方法,当页面解析时将调用此方法从设计时的Tag标记装载Items ,并且定义了一个索引器,另外还重载了基类中的NewItem用来返回WinPopItem类型。
最后是WinPop组件的实现
using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
namespace Keyss.WebControls
{
[DefaultProperty("Items"),
ToolboxData("<{0}:WinPop runat=server></{0}:WinPop>")]
public class WinPop : System.Web.UI.WebControls.WebControl
{
loaddata from table#region loaddata from table
public virtual void LoadFromTable(System.Data.DataTable table)
{
try
{
this.Items.Clear();
foreach(System.Data.DataRow row in table.Rows)
{
Keyss.WebControls.WinPopItem item = new Keyss.WebControls.WinPopItem();
item.LoadFromDataRow(row);
this.Items.Add(item);
}
}
catch
{
throw new ArgumentException("Table or filed name error!",table.TableName);
}
}
#endregion
items#region items
private WinPopItemCollection _items;
[
NotifyParentProperty(true),
PersistenceMode(PersistenceMode.InnerProperty),
DesignerSerializationVisibility(DesignerSerializationVisibility.Content),
]
public WinPopItemCollection Items
{
get
{
if(this._items ==null)
{
_items = new WinPopItemCollection();
if(this.IsTrackingViewState)
_items.TrackViewState();
}
return _items;
}
}
#endregion
render#region render
protected override void OnPreRender(EventArgs e)
{
System.Text.StringBuilder codesource = new System.Text.StringBuilder();
if((this.Enabled)&&(!this.Items.IsEmpty)&&(!Page.IsStartupScriptRegistered(this.ClientID)))
{
codesource.Append("<script language=\"JavaScript\" >");
codesource.Append("\n\r");
for(int i=0;i<Items.Count;i++)
{
codesource.Append(Items[i].GetPreRenderJScript());
}
codesource.Append("</script>");
Page.RegisterStartupScript(ClientID,codesource.ToString());
}
}
protected override void Render(HtmlTextWriter writer)
{
if((this.Site!=null)&&this.Site.DesignMode)
writer.WriteLine(this.ID);
}
#endregion
view State#region view State
protected override void LoadViewState(object state)
{
if(state !=null)
{
object[] savedState = (object[])state;
if(savedState[0] != null)
base.LoadViewState(savedState[0]);
if(savedState[1] != null)
Items.LoadViewState(savedState[1]);
}
}
protected override object SaveViewState()
{
object[] savedState = new object[2];
savedState[0] = base.SaveViewState ();
savedState[1] = Items.SaveViewState();
for(int i=0;i<savedState.Length;i++)
{
if(savedState[i] != null)
return savedState;
}
return null;
}
protected override void TrackViewState()
{
base.TrackViewState();
Items.TrackViewState();
}
#endregion
}
}
从源程序中可以看到WinPop中含有一个默认属性Items并且重载了基类中的视图状态函数,用来支持Items的视图状态。为了加载方便还实现了LoadFromTable方法用来从datatable中载入Items数据。由于WinPop并没有实际的HTML元素,只是在页面中注册jscript角本,所以重载的Render方法只是判断是否为设计时,如果是则输出控件的ID便于设计时选择该控件,而在OnPreRender方法中则注册startup类型的jscript块,这样当页面绘制时,控件会根据Items中的内容绘制一系列的window.open(url, target, features, replace);来实现弹出窗口的目的。