2011年6月22日 星期三

如何建立樹狀(分層)資料繫結控制項?

樹狀資料繫結控制項一般呈現為樹狀結構,使用的資料來源也是具有樹狀結構的資料,像是XML或是SiteMap等資料。

簡而言之,只要2個步驟:

  1. 繼承自HierarchicalDataBoundControl
  2. 改寫PerformDataBinding(),取得資料來源,並進行資料繫結。

第2步驟相當於一般資料繫結控制項的PerformSelect()和PerformDataBinding()。

為什麼不是使用PerformSelect()和PerformDataBinding()?關鍵在於"樹狀"結構,必須要使用遞迴來處理,一層一層的往下構建。

在細節方面,有幾個地方需要注意:

  1. 如果已經初始化資料繫結控制項,而資料來源在PreRender事件後變更,那麼就必須要執行OnDataPropertyChanged()方法。
    if (Initialized) //BaseDataBoundControl.Initialized
    {
        OnDataPropertyChanged(); //BaseDataBoundControl.OnDataPropertyChanged()
    }

  2. 需使用樹狀列舉IHierarchicalEnumerable介面來處理資料。包括IHierarchicalEnumerable.GetHierarchyData()方法與IHierarchyData介面。
    private void RecurseDataBindInternal(TreeNode RootNode, IHierarchicalEnumerable enumerable, int depth)
    {
        foreach (object item in enumerable)
        {
            IHierarchyData data = enumerable.GetHierarchyData(item);
            ...
        }
    }

  3. 如果有設定DataTextField就使用DataBinder.GetPropertyValue()來取取得屬性,不然就用TypeDescriptor.GetProperties()來做。
    if (DataTextField.Length > 0)
    {
        System.Xml.XmlElement elm = data.Item as System.Xml.XmlElement;
        if (elm.HasAttributes)
        {
            newNode.Text = DataBinder.GetPropertyValue(data, DataTextField, null);
        }
        else
        {
            newNode.Text = "(Family)";
        }
    }
    else
    {
        PropertyDescriptorCollection props = TypeDescriptor.GetProperties(data);
        newNode.Text = string.Empty;
        if (props.Count >= 1)
        {
            if (null != props[0].GetValue(data).ToString())
            {
                newNode.Text = props[0].GetValue(data).ToString();
            }
        }
    }

  4. 遞迴使用自訂義RecurseDataBindInternal()方法來新增結點並進行資料繫結。
    IHierarchicalEnumerable enumerable = view.Select();
    if (enumerable != null)
    {
        Nodes.Clear();
        try
        {
            RecurseDataBindInternal(RootNode, enumerable, 1);
        }
        catch(Exception e)
        {
        }
    }
主要的程式碼如下(已修正MSDN中屬性的BUG):
namespace Samples.AspNet.CS.Controls
{    
    [ToolboxData("<{0}:GeneologyTree runat=server></{0}:GeneologyTree>")]
    public class GeneologyTree:HierarchicalDataBoundControl
    {
        private TreeNode rootNode;
        public TreeNode RootNode
        {
            get
            {
                if (rootNode == null)
                {
                    rootNode = new TreeNode(string.Empty);
                }
                return rootNode;
            }
        }
        private ArrayList nodes;
        public ArrayList Nodes
        {
            get
            {
                if (nodes == null)
                {
                    nodes = new ArrayList();
                }
                return nodes;
            }
        }
        public string DataTextField
        {
            get
            {
                object o = ViewState["DataTextField"];
                return (o == null) ? string.Empty : (string)o;
            }
            set
            {
                ViewState["DataTextField"] = value;
                if (Initialized) //BaseDataBoundControl.Initialized
                {
                    OnDataPropertyChanged(); //BaseDataBoundControl.OnDataPropertyChanged()
                }
            }
        }
        private int _maxDepth = 0;
        protected override void PerformDataBinding()
        {
            base.PerformDataBinding();
            if (!IsBoundUsingDataSourceID && DataSource == null)
            {
                return;
            }
            HierarchicalDataSourceView view = GetData(RootNode.DataPath);
            if (view == null)
            {
                throw new InvalidOperationException("No view returned by data source control.");
            }
            IHierarchicalEnumerable enumerable = view.Select();
            if (enumerable != null)
            {
                Nodes.Clear();
                try
                {
                    RecurseDataBindInternal(RootNode, enumerable, 1);
                }
                catch(Exception e)
                {
                }
            }
        }
        private void RecurseDataBindInternal(TreeNode RootNode, IHierarchicalEnumerable enumerable, int depth)
        {
            foreach (object item in enumerable)
            {
                IHierarchyData data = enumerable.GetHierarchyData(item);
                if (null != data)
                {
                    TreeNode newNode = new TreeNode();
                    RootViewNode rvNode = new RootViewNode();
                    rvNode.Node = newNode;
                    rvNode.Depth = depth;
                    if (DataTextField.Length > 0)
                    {
                        System.Xml.XmlElement elm = data.Item as System.Xml.XmlElement;
                        if (elm.HasAttributes)
                        {
                            newNode.Text = DataBinder.GetPropertyValue(data, DataTextField, null);
                        }
                        else
                        {
                            newNode.Text = "(Family)";
                        }
                    }
                    else
                    {
                        PropertyDescriptorCollection props = TypeDescriptor.GetProperties(data);
                        newNode.Text = string.Empty;
                        if (props.Count >= 1)
                        {
                            if (null != props[0].GetValue(data).ToString())
                            {
                                newNode.Text = props[0].GetValue(data).ToString();
                            }
                        }
                    }
                    nodes.Add(rvNode);
                    if (data.HasChildren)
                    {
                        IHierarchicalEnumerable newEnumerable = data.GetChildren();
                        if (newEnumerable != null)
                        {
                            RecurseDataBindInternal(newNode, newEnumerable, depth + 1);
                        }
                    }
                    if (_maxDepth < depth) _maxDepth = depth;
                }
            }
        }
        protected override void RenderContents(HtmlTextWriter writer)
        {
            writer.Write("<PRE>");
            int currentDepth = 1;
            int currentTextLength = 0;
            foreach (RootViewNode rvNode in Nodes)
            {
                if (rvNode.Depth != currentDepth)
                {
                    writer.WriteLine(""); //子系需換行
                    int halfLine = currentTextLength / 2;
                    string indent = new string(' ', halfLine);
                    writer.WriteLine(indent + "|"); //子系需縮排並加上連接線
                    ++currentDepth;
                    currentTextLength = 0;
                    writer.Write(indent);
                }
                string output = " " + rvNode.Node.Text + " ";
                writer.Write(output);
                currentTextLength = currentTextLength + output.Length;
            }
            writer.Write("</PRE>");
        }
    }
}
使用的XML檔案(geneology.xml):
<?xml version="1.0" encoding="utf-8" ?>
<family>
    <member title="great-grandfather">
        <member title="grandfather" >
            <member title="child" />
            <member title="father" >
                <member title="son" />
            </member>
        </member>
    </member>
</family>
測試的網頁:
<cc1:GeneologyTree ID="GeneologyTree1" runat="server" 
        DataSourceID="XmlDataSource1" DataTextField="title" />
<asp:XmlDataSource ID="XmlDataSource1" runat="server" 
    DataFile="~/App_Data/geneology.xml"></asp:XmlDataSource>

 


測試的結果:


image


參考資料:



 


2011年6月10日 星期五

[WinDBG]搜尋路徑設定

[File]->[Symbol File Path…]

C:\Windows\symbols;C:\MySymbols;srv*C:\Windows\symbols*http://msdl.microsoft.com/download/symbols

srv*本機符號快取資料夾*http://msdl.microsoft.com/download/symbols

偵錯符號會下載到『本機符號快取資料夾』這個位置。

C:\MySymbols放置我們需要進行偵錯或使用的檔案,例如.pdb和.cs檔案等。

[File]->[Source File Path…]

C:\MySymbols

[File]->[Image File Path…]

C:\MySymbols

設定完之後,可以再命令列中輸入:

!sym noisy

當執行過程時引用到符號檔時,會顯示出找到的路徑或找不到的訊息提示。

設定沒有問題時,可以再輸入:

!sym quiet

來關閉符號訊息引用的輸出。

參考資料:

2011年6月7日 星期二

如何建立資料繫結控制項?

建立一個資料繫結控制項也就是建立一個使用資料來源的控制項(當然,你必須提供一個資料來源給他使用)。

簡而言之,只要三個步驟:

  1. 繼承DataBoundControl
  2. 改寫PerformSelect(),從資料來源取得資料來源視圖(DataSourceView)
  3. 改寫PerformDataBinding(),將資料指定給控制項的屬性。

但細節方面,也有幾個地方要注意:

  1. 如果已經初始化資料繫結控制項,而資料來源在PreRender事件後變更,那麼就必須要執行OnDataPropertyChanged()方法。
  2. if (Initialized) //BaseDataBoundControl.Initialized
    {
        OnDataPropertyChanged(); //BaseDataBoundControl.OnDataPropertyChanged()
    }

  3. 如果是使用DataSourceID來指定資料來源,那麼必須在取得資料來源視圖後執行OnDataBinding()方法;
  4. 如果是使用DataSource來指定資料來源,則在取得資料來源視圖前執行OnDataBinding()方法。
  5. if (!IsBoundUsingDataSourceID) //BaseDataBoundControl.IsBoundUsingDataSourceID
    {
        OnDataBinding(EventArgs.Empty);  //Control.OnDataBinding()
    }
    GetData().Select(DataSourceSelectArguments.Empty,
        (data) => {
            if (IsBoundUsingDataSourceID)
                OnDataBinding(EventArgs.Empty);
            PerformDataBinding(data);
        });

  6. 需要設定資料繫結的狀態,以告訴類別是否需要設定其他的資料。
  7. RequiresDataBinding = false;  //指出是否應該呼叫DataBind()方法
    MarkAsDataBound();  //將檢視狀態中的控制項狀態設為已成功繫結至資料
    OnDataBound(EventArgs.Empty);  //引發 DataBound 事件

 


主要的程式碼如下:

[DefaultProperty("Text")]
[ToolboxData("<{0}:TextBoxSet runat=server></{0}:TextBoxSet>")]
public class TextBoxSet : DataBoundControl
{
    private IList _boxSet = null;
    public IList BoxSet
    {
        get
        {
            if (null == _boxSet)
            {
                _boxSet = new ArrayList();
            }
            return _boxSet;
        }
    }
    public string DataTextField
    {
        get 
        {
            object o = ViewState["DataTextField"];
            return (o == null) ? string.Empty : (string)o;
        }
        set
        {
            ViewState["DataTextField"] = value;
            //BaseDataBoundControl.Initialized
            //取得值,指出是否已初始化資料繫結控制項。
            if (Initialized)
            {
                //BaseDataBoundControl.OnDataPropertyChanged()
                //在其中一個基底資料來源識別屬性變更之後,將資料繫結控制項重新繫結至其資料。
                //如果 DataSource、DataSourceID 或 DataMember 屬性值在發生頁面的 PreRender 事件後做了變更,則會呼叫 OnDataPropertyChanged 方法。 
                OnDataPropertyChanged();
            }
        }
    }
    protected override void PerformSelect()
    {
        //BaseDataBoundControl.IsBoundUsingDataSourceID
        //取得值,指出 System.Web.UI.WebControls.BaseDataBoundControl.DataSourceID 屬性是否已設定。
        if (!IsBoundUsingDataSourceID)
        {
            //Control.OnDataBinding()
            //引發 System.Web.UI.Control.DataBinding 事件。
            OnDataBinding(EventArgs.Empty);
        }
        //DataBoundControl.GetData()
        //擷取 System.Web.UI.DataSourceView 物件,執行資料作業時資料繫結控制項會使用它。
        //DataSourceView.Select()
        //從基礎資料儲存區非同步取得資料清單。
        GetData().Select(DataSourceSelectArguments.Empty, new DataSourceViewSelectCallback(OnDataSourceViewSelectCallback));
        //BaseDataBoundControl.RequiresDataBinding
        //取得或設定值,指出是否應該呼叫 System.Web.UI.WebControls.BaseDataBoundControl.DataBind()方法 
        RequiresDataBinding = false;
        //DataBoundControl.MarkAsDataBound()
        //將檢視狀態中的控制項狀態設為已成功繫結至資料。
        MarkAsDataBound();
        //BaseDataBoundControl.OnDataBound
        //引發 System.Web.UI.WebControls.BaseDataBoundControl.DataBound 事件。
        OnDataBound(EventArgs.Empty);
    }
    private void OnDataSourceViewSelectCallback(IEnumerable data)
    {
        if (IsBoundUsingDataSourceID)
            OnDataBinding(EventArgs.Empty);
        PerformDataBinding(data);
    }
    protected override void PerformDataBinding(IEnumerable data)
    {
        //DataBoundControl.PerformDataBinding
        //在衍生類別中覆寫時,會將資料從資料來源繫結至控制項。
        base.PerformDataBinding(data);
        if (data != null)
        {
            foreach (object dataItem in data)
            {
                TextBox box = new TextBox();
                if (DataTextField.Length > 0)
                {
                    box.Text = DataBinder.GetPropertyValue(dataItem, DataTextField, null);
                }
                else
                {
                    PropertyDescriptorCollection pdcs = TypeDescriptor.GetProperties(dataItem);
                    box.Text = string.Empty;
                    if (pdcs.Count >= 1)
                    {
                        if (null != pdcs[0].GetValue(dataItem))
                        {
                            box.Text = pdcs[0].GetValue(dataItem).ToString();
                        }
                    }
                }
                BoxSet.Add(box);
                
            }
        }
    }
    protected override void Render(HtmlTextWriter writer)
    {
        if (BoxSet.Count <= 0)
        {
            return;
        }
        writer.RenderBeginTag(HtmlTextWriterTag.Ul);
        foreach (object item in BoxSet)
        {
            TextBox box = item as TextBox;
            writer.RenderBeginTag(HtmlTextWriterTag.Li); //<li>
            writer.AddAttribute(HtmlTextWriterAttribute.Type, "text");
            writer.AddAttribute(HtmlTextWriterAttribute.Value, box.Text);
            writer.RenderBeginTag(HtmlTextWriterTag.Input);
            writer.RenderEndTag(); //</>
            writer.RenderEndTag(); //</li>
        }
        writer.RenderEndTag();            
    }
其中
GetData().Select(DataSourceSelectArguments.Empty, new DataSourceViewSelectCallback(OnDataSourceViewSelectCallback));
也等同於
GetData().Select(DataSourceSelectArguments.Empty, OnDataSourceViewSelectCallback);


或是

GetData().Select(DataSourceSelectArguments.Empty,
    (data) => {
        if (IsBoundUsingDataSourceID)
            OnDataBinding(EventArgs.Empty);
        PerformDataBinding(data);
    });
不過,我還是覺得使用new的方式比較好,可讀性佳,至少你會比較了解你到底是在處理哪一種類別。

測試的網頁如下:

<asp:AccessDataSource ID="AccessDataSource1" runat="server" 
    DataFile="~/App_Data/NorthWind.mdb" 
    SelectCommand="SELECT [LastName] FROM [Employees]"></asp:AccessDataSource>
<cc1:TextBoxSet ID="TextBoxSet1" runat="server"
                DataSourceID="AccessDataSource1" />
<hr />
<asp:AccessDataSource ID="AccessDataSource2" runat="server" 
    DataFile="~/App_Data/NorthWind.mdb" 
    SelectCommand="SELECT * FROM [Employees]"></asp:AccessDataSource>
<cc1:TextBoxSet ID="TextBoxSet2" runat="server"
 DataSourceID="AccessDataSource2" DataTextField="FirstName" />
<hr />
<cc1:TextBoxSet ID="TextBoxSet3" runat="server" DataTextField="Name" />
<hr />

使用的資料庫可自微軟下載:Access 2000 Tutorial: Northwind Traders Sample Database


其中第三個控制項使用DataSource做為資料來源,我們在Page_Load事件時提供給他

protected void Page_Load(object sender, EventArgs e)
{
    var data = new object[]
    {
        new {ProductId = 1, Name = "可可"},
        new {ProductId = 2, Name = ""},
        new {ProductId = 3, Name = "咖啡"}
    };
    this.TextBoxSet3.DataSource = data;
    this.TextBoxSet3.DataBind();
}

測試結果:


image


 


參考資料: