[ASP.NET]打造拖曳版的TreeView - With C# and VB.NET

想讓原生的TreeView控制項支援拖曳,首先得先滿足幾個要件。
1、必須擁有一個支援拖曳的JavaScript Library。
2、必須能將mousedown、mouseup事件掛到TreeNode上。
3、必須能在Drop時,進行Postback動作,讓後端得知Node的移動。

 

打造拖曳版的TreeView - With C# and VB.NET  
             TreeView With Drag&Drop
 
 
/黃忠成
 
 
 
從兩段影片開始
 
 
 
 
TreeView可拖曳需克服的困難
 
 
想讓原生的TreeView控制項支援拖曳,首先得先滿足幾個要件。
 
1、必須擁有一個支援拖曳的JavaScript Library
2、必須能將mousedownmouseup事件掛到TreeNode上。
3、必須能在Drop時,進行Postback動作,讓後端得知Node的移動。
 
第一點對我來說並不難,因為之前撰寫 ASP.NET AJAX 一書時,就已經提供了 GridView 標頭移位的例子,其中的拖曳 Javascript Library 可以直接拿來修改。
 
第二點比較麻煩,因為 TreeView 有個特別的地方,每個 Node Client ID 皆是隱藏的,所以無法像使用 GridView 般,在 RowCreated 處加上 mousedown mouseup 事件。
 
第三點不困難,只要能解決前兩點,那麼在 Drop 時觸發 PostBack ,告知後端 Node 的移動,就只是短短一行 __doPostBack 程式碼而已,問題在, ASP.NET PostBack 機制有著驗證機制,在 PostBack 時不能隨意指定參數,這點可透過 RegisterForEventValidation 函式,搭配 Hidden Field 解決。
 
 
 
 TreeView的序號產生規則
 
所以,總結前面三點的討論,對我而言,第二點是最棘手的!在觀察 TreeView 的運行邏輯,發現到 TreeNode 有一個內部用屬性,名為 SelectID ,其內的值即是每個 Node ClientID ,如果可以取到這個值,那麼要為 Node 掛上 mousedown mouseup 事件就不困難了。
  
PropertyInfo pi = typeof(TreeNode).GetProperty("SelectID",
         BindingFlags.NonPublic | BindingFlags.Instance);
string value = pi.GetValue(node, null) as string;
    
不過這還有個問題,這個 SelectID 只存在於 PreRenderComplete 事件區間,其它如 Load PreRender 時,每個 TreeNode 的此值都是一樣的。以下的程式碼展示如何掛上 mousedown 事件到每個 Node
  
private void HookPreRenderComplete(object sender, EventArgs args)
{
..........
            if (_view.Nodes.Count > 0)
            {
                foreach (TreeNode node in _view.Nodes)
                {                   
                    PropertyInfo pi = typeof(TreeNode).GetProperty("SelectID",
                            BindingFlags.NonPublic | BindingFlags.Instance);
                    string value = pi.GetValue(node, null) as string;
                    ..........................
                    ...........................
                    _page.ClientScript.RegisterStartupScript(typeof(Page),
                          "TreeViewFuncHook" + value,
                         string.Format("document.getElementById('{0}')"+
                                       ".onmousedown=createDelegate({0}_dragObj,"+
                                       {0}_dragObj.initializedragns);\n", value));
                    ...........................
            }           
 }
   
  接下來才是最大的問題,記得前面提過, SelectID 的值僅在 PreRenderComplete 才有意義,因此當使用者完成拖曳後,即使我們將拖曳的來源及目地 Node ID 放在 Hidden Field 傳回,然後於 Page_Load 中收取,那接下來又該如何比對拖曳的 Node 呢?在 Page_Load 中並無法得知 SelectID 真正的值不是嗎?是的,此時就得更深入的了解 TreeView Node ID 產生邏輯。 
  其實說穿了很簡單, TreeView Node ID 產生邏輯是遞增型的,第一個 Node ID 會是 <TreeView ID>+t+<index> ,第二個則是 <TreeView ID>+t+<index+1> ,以此類推,如果第一個 Node 有一個子 Node ,那麼結果將如下所示。
 
Node 1 (TreeView1t0)
       Node11(TreeView1t1)
Node2(TreeView1t2)
 
看起來不難是吧,只要能模擬 TreeView 生出 Node ID 的規則,那麼要比對拖曳的 Node ID 就不困難了。
  
int _gIndex = 0;
...................
_gIndex = 0;
foreach (TreeNode node in _view.Nodes)
{
       sourceNode = FindNode(arguments[1], node, true);
       if (sourceNode != null)
            break;
}
 
_gIndex = 0;
foreach (TreeNode node in _view.Nodes)
{
        destNode = FindNode(arguments[2], node, true);
        if (destNode != null)
            break;
}                   
....................
 
private TreeNode FindNode(string nodeID, TreeNode parent, bool checkParent)
{
     string value = _view.ClientID + "t" + _gIndex.ToString();
     if (checkParent)
     {
        if (value == nodeID)
            return parent;
         _gIndex++;
      }
      foreach (TreeNode cNode in parent.ChildNodes)
     {
         value = _view.ClientID + "t" + _gIndex.ToString();
         TreeNode retNode = null;
         if (value == nodeID)
              return cNode;
          _gIndex++;
         if (cNode.ChildNodes.Count > 0)
         {
            retNode = FindNode(nodeID, cNode, false);
            if (retNode != null)
                return retNode;
         }
      }
 return null;
}
  
 
開始拖曳-pasp_aspnet_drag_manager.js
 
 
好了,解決掛上 mousedown mouseup 事件及比對回傳值的問題後,剩下的就是完成拖曳動作的核心, Javascript 了。 
拖曳動作其實不外乎是,滑鼠於 Node 上按下, mousedown 事件觸發,記錄開始拖曳的 Node ID,然後進入拖曳模式,此時要特別注意,我們得 Hook docuement mousemove 動作,而不是 Node mousemove  
當使用者放掉滑鼠鍵時, Node 的 mouseup 觸發,此時觸發 mouseup 事件的 Node 即是拖曳的目的地,這時將拖曳開始的 Node ID 目的地的 Node ID 放入 Hidden Field ,然後觸發 PostBack 傳回,完成整個拖曳動作。 
整個拖曳動作的靈魂在於 pasp_aspnet_drag_manager.js 檔案中,當開始拖曳時,視瀏覽器的不同, initializedragie 或是 initializedragns 會被呼叫。
  
his.initializedragie = function() {
        if (!this.enableDrag) return;
        iex = event.clientX;
        iey = event.clientY;
        tempx = event.clientX;
        tempy = event.clientY;
        this.panelCtrl.style.position = 'absolute';
        this.panelCtrl.style.pixelLeft =
            (tempx + document.documentElement.scrollLeft + document.body.scrollLeft);
        this.panelCtrl.style.pixelTop =
            (tempy + document.documentElement.scrollTop + document.body.scrollTop) - 5;
        Orphean.Framework.Web.DragManager.currentPanel = this;
        this.dragapproved = true;
        this.panelCtrl.innerHTML = this.containerCtrl.innerHTML;
        document.onmousemove = Orphean$Framework$Web$DragManager$Panel_drag_dropie_Proxy;
        this.panelCtrl.onmouseup = Orphean$Framework$Web$DragManager$Panel_stopie_Proxy;
        document.onmouseup = Orphean$Framework$Web$DragManager$Panel_stopie_Proxy;
        this.DisplayContent(true);
        document.onselectstart = function() { return false; } // ie
    }
 
    this.initializedragns = function(evt) {
        if (!this.enableDrag) return;
        iex = evt.clientX;
        iey = evt.clientY;
        tempx = iex;
        tempy = iey;
        this.panelCtrl.style.position = 'absolute';
        this.panelCtrl.style.left = window.scrollX + tempx + 20 + "px";
        this.panelCtrl.style.top = (window.scrollY + tempy) - 5 + "px";
        Orphean.Framework.Web.DragManager.currentPanel = this;
        this.dragapproved = true;
        this.panelCtrl.innerHTML = this.containerCtrl.innerHTML;
        window.captureEvents(Event.MOUSEMOVE | Event.MOUSEUP);
        document.onmousemove = Orphean$Framework$Web$DragManager$Panel_drag_dropie_Proxy;
        this.panelCtrl.onmouseup = Orphean$Framework$Web$DragManager$Panel_stopie_Proxy;
        document.onmouseup = Orphean$Framework$Web$DragManager$Panel_stopie_Proxy;
        this.DisplayContent(true);
        evt.preventDefault(); //Firefox
    }
  
這兩個函式的動作一樣,僅視瀏覽器不同而使用不同的語法罷了,特別注意其中兩個成員變數, containerCtrl 指的是拖曳的 Node panelCtrl 指的則是一個 div Element 。
 是的!當使用者開始拖曳時,拖的並不是原來的 Node ,而是一個由我們所準備的 div Element ,在 initializedrag 期間,我們將 containerCtrl innerHTML 複製到 div Element 中,讓使用者看起來像是在拖曳原來的 Node  
當拖曳開始時,所有的滑鼠移動動作都會觸發 mousemove 事件,再次提醒,此時觸發的 mousemove document.mousemove
  
function Orphean$Framework$Web$DragManager$Panel_drag_dropie_Proxy(evt)
{
 if(Orphean.Framework.Web.DragManager.currentPanel != null)
 {  
    if (navigator.userAgent.indexOf(' Firefox/') > -1)
        Orphean.Framework.Web.DragManager.currentPanel.drag_dropns(evt);
    else
        Orphean.Framework.Web.DragManager.currentPanel.drag_dropie();
 }
}
 
...............
 
   this.drag_dropie = function() {
        if (this.dragapproved == true) {
            this.panelCtrl.style.pixelLeft = (tempx + event.clientX +
                   document.documentElement.scrollLeft + document.body.scrollLeft) - iex;
            if (!this.disableVectDrag)
                this.panelCtrl.style.pixelTop = (tempy + event.clientY +
                  document.documentElement.scrollTop + document.body.scrollTop) - iey;  
                  //disable vect-move
        }
        return false;
    }
 
    this.drag_dropns = function(evt) {
        this.panelCtrl.style.left = (tempx + evt.clientX + window.scrollX) - iex + 20 + "px";
        if (!this.disableVectDrag)
            this.panelCtrl.style.top = (tempy + evt.clientY + window.scrollY) - iey + "px";
        return false;
    }
  
drag_dropie drag_dropns 所做的事是隨滑鼠移動,改變我們複製 Node 內容而來之 div Elemtnt 的位置。
當拖曳結束後,指定的mouseup事件就會被觸發。
  
function Orphean$Framework$Web$DragManager$Panel_stopie_Proxy()
{
   if (Orphean.Framework.Web.DragManager.currentPanel != null) {
       Orphean.Framework.Web.DragManager.currentPanel.DisplayContent(false);   
   if (navigator.userAgent.indexOf(' Firefox/') == -1)
        document.onselectstart =
              Orphean.Framework.Web.DragManager.currentPanel.old_select_func;
    if (getInternetExplorerVersion() >= 8 || (navigator.userAgent.indexOf(' Firefox/') != -1))     
    {
        Orphean$Framework$Web$DragManagerMoveNodeIE8();
    }
    Orphean.Framework.Web.DragManager.currentPanel.dragapproved=false;
    Orphean.Framework.Web.DragManager.currentPanel.panelCtrl.innerHTML = '';
    Orphean.Framework.Web.DragManager.currentPanel.stop();
 }
}
   
不過這有個問題點,在 IE7 的狀態下,目標 Node mouseup 事件及我們的 div element mouseup 事件皆會觸發,但在 IE8 FireFox 情況下,只有我們 div Element mouseup 事件會被觸發,目標 Node mouseup 事件會被吃掉。
 因此,現在我們所遭遇到的問題是,如果只有我們div elementmouseup會被觸發,那如何得知放掉滑鼠的目標Node
 
解決辦法說來也很簡單,雖然 Node mouseup 事件會被吃掉,但當滑鼠移到 Node 上時, Node mousemove 是正常觸發的,所以,我們只要逐一記錄 mousemove 時的 Node 即可。
 
function Orphean$Framework$Web$DragManager$LastMove() {
    Orphean.Framework.Web.DragManager.lastMove = event.srcElement;
}
 
function Orphean$Framework$Web$DragManager$LastMoveNS(evt) {
    Orphean.Framework.Web.DragManager.lastMove = evt.target;
}
 
當掛載 Node mouseup mousedown 事件時,也順便將 mousemove 掛上。
 最後的 Drop 程式碼就如下。 
 
function Orphean$Framework$Web$DragManagerMoveNode() {
    if (Orphean.Framework.Web.DragManager.currentPanel != null) {
        document.getElementById("dragTreeView_$$_source").value =
           Orphean.Framework.Web.DragManager.currentPanel.containerCtrl.id;
        document.getElementById("dragTreeView_$$_target").value = event.srcElement.id;
         __doPostBack(Orphean.Framework.Web.DragManager.currentPanel.postBackOwner,
             "NodeExchange");
        Orphean$Framework$Web$DragManager$Panel_stopie_Proxy();
    }
 
}
 
function Orphean$Framework$Web$DragManagerMoveNodeIE8() {
    if (Orphean.Framework.Web.DragManager.currentPanel != null && Orphean.Framework.Web.DragManager.lastMove != null) {
        document.getElementById("dragTreeView_$$_source").value =
            Orphean.Framework.Web.DragManager.currentPanel.containerCtrl.id;
        document.getElementById("dragTreeView_$$_target").value =
                Orphean.Framework.Web.DragManager.lastMove.id;
        __doPostBack(Orphean.Framework.Web.DragManager.currentPanel.postBackOwner,
               "NodeExchange");
        Orphean.Framework.Web.DragManager.lastMove = null;
    }
}
 
 其中的 dragTreeView_$$ 是用來記錄拖曳及目標 Node ID Hidden Field PostBack 時會將此值回傳。
 
 
 
PostBack to Server
 
 
Server PostBack 的處理就簡單的多了,不外乎是由 Hidden Field 取出拖曳 Node 及目標 Node ID ,然後進行移位。但在這之前,還有兩個問題要處理,第一是 PostBack 時,我們指定了 NodeExchange PostBack 參數,但 ASP.NET TreeView 是受 Event Validation 所保護的,當 PostBack 發生時, ASP.NET 將會拋出 Event Validation Fail 的錯誤訊息。
解決辦法很簡單,要嘛將 Page EnableEventValidation 設為 false ,要嘛就告訴 ASP.NET TreeView NodeExchange 這個參數。
  
public void RegisterEventValidation()
{
      _page.ClientScript.RegisterForEventValidation(_view.UniqueID, "NodeExchange");
}
  
注意, RegisterForEventValidation 只能在 Render 期間呼叫,所以你必須 override Page Render 方法。
 
protected override void Render(HtmlTextWriter writer)
{          
    _viewExtender.RegisterEventValidation();
    base.Render(writer);           
}
  
雖然有方法可以欺騙 ASP.NET ,連這點都跳過,不過我們還是照規矩來較為保險。
 
 
 
來自 TreeView 原始設計的限制
 
 到目前為止,處理 PostBack 的程式碼概念已漸趨完善,下方是其原始碼。
  
private void HookLoad(object sender,EventArgs args)
{
     if (_page.IsPostBack)
     {
         Page page = _page;
         string target = page.Request.Params["__EVENTTARGET"];
         if (target == _view.ClientID && page.Request.Params["__EVENTARGUMENT"] == "NodeExchange")
        { 
               string[] arguments = new string[] { "DRC",
                        page.Request.Params["dragTreeView_$$_source"],
                        page.Request.Params["dragTreeView_$$_target"] };
                if (arguments.Length == 0 || arguments[0] != "DRC") return;
                  
                TreeNode sourceNode = null;
                TreeNode destNode = null;
 
                _gIndex = 0;
                foreach (TreeNode node in _view.Nodes)
                {
                   sourceNode = FindNode(arguments[1], node, true);
                   if (sourceNode != null)
                    break;
                }
 
                _gIndex = 0;
                foreach (TreeNode node in _view.Nodes)
                {
                     destNode = FindNode(arguments[2], node, true);
                     if (destNode != null)
                         break;
                }                   
 
                if (sourceNode != destNode)
                {
                   if (destNode != null)
                       OnNodeChanging(new NodeChangingEventArgs(sourceNode, destNode));
                        List<TreeNode> list = new List<TreeNode>();
                        foreach (TreeNode node in _view.Nodes)
                        {
                            TreeNode newNode = new TreeNode(node.Text,node.Value,
                                 node.ImageUrl,node.NavigateUrl,node.Target);
                            CopyNode(node, newNode);
                            list.Add(newNode);
                            if (node.ChildNodes.Count > 0)
                                DeepCopy(newNode, node);
                        }
                        _view.Nodes.Clear();
                        foreach (TreeNode node in list)
                            _view.Nodes.Add(node);
                }                   
           }
      }
 }
    
 這片段程式碼有兩點需要特別解釋,第一是 OnNodeChanging 事件觸發,我把 Node 移動的動作交給掛載事件端處理,這樣設計師可以自行決定那個 Node 該移到那個 Node 或是拒絕某個 Node 移到某個 Node 的動作,甚至還能進行複製 Node 的行為。
 第二點是, TreeView 原始的設計並未考慮到我們會將 Node 搬來搬去,所以如果只是單純的將一個 Node 由一個 Parent Node 移走,加入成另一個 NodeChild Node 幾次後就會發生 index out of range 錯誤訊息。所以此處選擇在每次 Node 移動後,將原來的 Node 複製出來,將整個 TreeView Node 清空,最後再加回去,以此躲避 TreeView 內部 Log 處理失誤所造成的 out of range 錯誤訊息。
 
 
 
兜在一起,目賭 TreeViewDragExtender 的誕生
 
好了, TreeView 的拖曳相關難題皆已解決,剩下的就是怎麼把這些程式碼包裝起來,讓用起來更方便,請觀賞 TreeViewDragExtender 的誕生。
  
TreeViewDragExtender.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Reflection;
using System.Security.Permissions;
 
namespace WebApplication6
{
    public class NodeChangingEventArgs : EventArgs
    {
        private TreeNode _sourceNode;
        private TreeNode _destinationNode;
 
        public TreeNode SourceNode
        {
            get
            {
                return _sourceNode;
            }
        }
 
        public TreeNode DestinationNode
        {
            get
            {
                return _destinationNode;
            }
        }
 
        public NodeChangingEventArgs(TreeNode sourceNode, TreeNode destinationNode)
        {
            _sourceNode = sourceNode;
            _destinationNode = destinationNode;
        }
    }
 
    public delegate void NodeChangingHandler(object sender,NodeChangingEventArgs args);
 
    public class TreeViewDragExtender
    {
        private Page _page;
        private int _gIndex = 0;
        private TreeView _view = null;
        private List<string> _fixedNodes = new List<string>();
        private NodeChangingHandler _nodeChangingHandler = null;
 
 
        public event NodeChangingHandler Changing
        {
            add
            {
                _nodeChangingHandler =
                   (NodeChangingHandler)Delegate.Combine(_nodeChangingHandler, value);
            }
            remove
            {
                _nodeChangingHandler =
                    (NodeChangingHandler)Delegate.Remove(_nodeChangingHandler, value);
            }
        }
 
 
        public TreeViewDragExtender(Page page, TreeView view)
        {
            _page = page;
            _view = view;
            _page.PreRenderComplete += new EventHandler(HookPreRenderComplete);
            _page.Load += new EventHandler(HookLoad);
            _page.ClientScript.RegisterClientScriptInclude("TreeViewDragScriptLib",
                                                  "pasp_aspnet_drag_manager.js");
        }
 
        private void OnNodeChanging(NodeChangingEventArgs args)
        {
            if (_nodeChangingHandler != null)
                _nodeChangingHandler(this, args);
        }
 
        //fixedNodes allow you mark some nodes to be not dragable,it's specify by node.Text .
        public TreeViewDragExtender(Page page, TreeView view,
                             string[] fixedNodes):this(page,view)
        {
            foreach (string item in fixedNodes)
                _fixedNodes.Add(item);
        }
 
        private void HookLoad(object sender,EventArgs args)
        {
            if (_page.IsPostBack)
            {
                Page page = _page;
                string target = page.Request.Params["__EVENTTARGET"];
                if (target == _view.ClientID &&
                            page.Request.Params["__EVENTARGUMENT"] == "NodeExchange")
                {
                    string[] arguments = new string[] { "DRC",
                        page.Request.Params["dragTreeView_$$_source"],
                        page.Request.Params["dragTreeView_$$_target"] };
                    if (arguments.Length == 0 || arguments[0] != "DRC") return;
                  
                    TreeNode sourceNode = null;
                    TreeNode destNode = null;
 
                    _gIndex = 0;
                    foreach (TreeNode node in _view.Nodes)
                    {
                        sourceNode = FindNode(arguments[1], node, true);
                        if (sourceNode != null)
                            break;
                    }
 
                    _gIndex = 0;
                    foreach (TreeNode node in _view.Nodes)
                    {
                        destNode = FindNode(arguments[2], node, true);
                        if (destNode != null)
                            break;
                    }                   
 
                    if (sourceNode != destNode)
                    {
                        if (destNode != null)
                            OnNodeChanging(new NodeChangingEventArgs(sourceNode, destNode));                       
                        List<TreeNode> list = new List<TreeNode>();
                        foreach (TreeNode node in _view.Nodes)
                        {
                            TreeNode newNode = new TreeNode(node.Text,node.Value,
                                       node.ImageUrl,node.NavigateUrl,node.Target);
                            CopyNode(node, newNode);
                            list.Add(newNode);
                            if (node.ChildNodes.Count > 0)
                                DeepCopy(newNode, node);
                        }
 
                        _view.Nodes.Clear();
                        foreach (TreeNode node in list)
                            _view.Nodes.Add(node);
                    }                   
                }
            }
        }
 
        private void CopyNode(TreeNode source, TreeNode target)
        {
            target.SelectAction = source.SelectAction;
            target.Selected = source.Selected;
            target.PopulateOnDemand = source.PopulateOnDemand;
            target.ShowCheckBox = source.ShowCheckBox;
            target.ToolTip = source.ToolTip;
            target.Checked = source.Checked;
            target.Expanded = source.Expanded;
            target.ImageToolTip = source.ImageToolTip;
        }
 
        private void DeepCopy(TreeNode node,TreeNode source)
        {
            foreach (TreeNode item in source.ChildNodes)
            {
                TreeNode newNode = new TreeNode(item.Text, item.Value,
                           item.ImageUrl, item.NavigateUrl, item.Target);
                CopyNode(item, newNode);              
                node.ChildNodes.Add(newNode);
                if(item.ChildNodes.Count > 0)
                    DeepCopy(newNode,item);
            }
        }
 
        private void HookPreRenderComplete(object sender, EventArgs args)
        {
            CreateDragPanel();
            ReflectionPermission per = new ReflectionPermission(PermissionState.Unrestricted);
            per.Demand();
            if (_view.Nodes.Count > 0)
            {
                _page.ClientScript.RegisterStartupScript(typeof(Page),
                   "TreeViewWrapperBegin"+_view.ClientID,
                        "<script language='javascript'>\n");
                foreach (TreeNode node in _view.Nodes)
                {                   
                    PropertyInfo pi = typeof(TreeNode).GetProperty("SelectID",
                               BindingFlags.NonPublic | BindingFlags.Instance);
                    string value = pi.GetValue(node, null) as string;
                    _page.ClientScript.RegisterStartupScript(typeof(Page),
                                          "TreeViewDragObj" + value,
                        string.Format("var {0}_dragObj = " +
                  " new Orphean.Framework.Web.DragManager.DragControl(\"{1}\","+
                  "document.getElementById(\"dragPanel_$$_For_Drag_Control\"),"+
                  "document.getElementById(\"{0}\"),'',false);\n",
                                      value,_view.ClientID));
                    if (!_fixedNodes.Contains(node.Text))
                    {
                        if (_page.Request.UserAgent.IndexOf("Firefox") != -1)
                            _page.ClientScript.RegisterStartupScript(typeof(Page),
                                                "TreeViewFuncHook" + value,
                             string.Format("document.getElementById('{0}').onmousedown="+
                     "createDelegate({0}_dragObj,{0}_dragObj.initializedragns);\n", value));
                        else
                            _page.ClientScript.RegisterStartupScript(typeof(Page),
                                        "TreeViewFuncHook" + value,
                                 string.Format("document.getElementById('{0}').onmousedown"+
                 "=createDelegate({0}_dragObj,{0}_dragObj.initializedragie);\n", value));
                    }
                    _page.ClientScript.RegisterStartupScript(typeof(Page),
                          "TreeViewFuncHook" + value + "UP",
                    string.Format("document.getElementById('{0}').onmouseup="+
                     "Orphean$Framework$Web$DragManagerMoveNode;\n", value, _view.ClientID));
                    if (_page.Request.UserAgent.IndexOf("Firefox") != -1)
                        _page.ClientScript.RegisterStartupScript(typeof(Page),
                           "TreeViewFuncHook" + value + "UPS",
              string.Format("document.getElementById('{0}').onmousemove="+
                 "Orphean$Framework$Web$DragManager$LastMoveNS;\n", value, _view.ClientID));
                    else
                       _page.ClientScript.RegisterStartupScript(typeof(Page),
                          "TreeViewFuncHook" + value + "UPS",
                    string.Format("document.getElementById('{0}').onmousemove="+
                   "Orphean$Framework$Web$DragManager$LastMove;\n", value, _view.ClientID));
                    if (node.ChildNodes.Count > 0)
                        EnlistTreeViewScripts(node);
                }
                _page.ClientScript.RegisterStartupScript(typeof(Page),
                         "TreeViewWrapperEnd" + _view.ClientID, "</script>");
            }           
        }
 
        //treeview build nodes with increase index.
        //so we simulate this action to find node.
        private TreeNode FindNode(string nodeID, TreeNode parent, bool checkParent)
        {
            string value = _view.ClientID + "t" + _gIndex.ToString();
            if (checkParent)
            {
                if (value == nodeID)
                    return parent;
                _gIndex++;
            }
            foreach (TreeNode cNode in parent.ChildNodes)
            {
                value = _view.ClientID + "t" + _gIndex.ToString();
                TreeNode retNode = null;
                if (value == nodeID)
                    return cNode;
                _gIndex++;
                if (cNode.ChildNodes.Count > 0)
                {
                    retNode = FindNode(nodeID, cNode, false);
                    if (retNode != null)
                        return retNode;
                }
            }
            return null;
        }
 
        private void EnlistTreeViewScripts(TreeNode parent)
        {
            foreach (TreeNode node in parent.ChildNodes)
            {
                if (_fixedNodes.Contains(node.Text))
                {
                    if (node.ChildNodes.Count > 0)
                        EnlistTreeViewScripts(node);
                    continue;
                }
                PropertyInfo pi = typeof(TreeNode).GetProperty("SelectID",
                           BindingFlags.NonPublic | BindingFlags.Instance);
                string value = pi.GetValue(node, null) as string;
                _page.ClientScript.RegisterStartupScript(typeof(Page),
                                         "TreeViewDragObj" + value,
                        string.Format("var {0}_dragObj = new "+
                    "Orphean.Framework.Web.DragManager.DragControl(\"{1}\","+
                    "document.getElementById(\"dragPanel_$$_For_Drag_Control\"),"+
                    "document.getElementById(\"{0}\"),'',false);\n",
                     value, _view.ClientID));
                {
                    if (_page.Request.UserAgent.IndexOf("Firefox") != -1)
                        _page.ClientScript.RegisterStartupScript(typeof(Page),
                       "TreeViewFuncHook" + value,   
                     string.Format("document.getElementById('{0}').onmousedown="+
                     "createDelegate({0}_dragObj,{0}_dragObj.initializedragns);\n", value));
                    else
                        _page.ClientScript.RegisterStartupScript(typeof(Page),
                            "TreeViewFuncHook" + value,  
                  string.Format("document.getElementById('{0}').onmousedown="+
                    "createDelegate({0}_dragObj,{0}_dragObj.initializedragie);\n", value));
                }
                _page.ClientScript.RegisterStartupScript(typeof(Page),
                     "TreeViewFuncHook" + value + "UP",
                 string.Format("document.getElementById('{0}').onmouseup="+
                 "Orphean$Framework$Web$DragManagerMoveNode;\n", value, _view.ClientID));
                if (_page.Request.UserAgent.IndexOf("Firefox") != -1)
                        _page.ClientScript.RegisterStartupScript(typeof(Page),
                         "TreeViewFuncHook" + value + "UPS",
                       string.Format("document.getElementById('{0}').onmousemove="+
                      "Orphean$Framework$Web$DragManager$LastMoveNS;\n",
                         value, _view.ClientID));
                    else
                       _page.ClientScript.RegisterStartupScript(typeof(Page),
                        "TreeViewFuncHook" + value + "UPS",
                        string.Format("document.getElementById('{0}').onmousemove="+
                        "Orphean$Framework$Web$DragManager$LastMove;\n",
                                  value, _view.ClientID));
                if (node.ChildNodes.Count > 0)
                    EnlistTreeViewScripts(node);
            }
        }
 
        private void CreateDragPanel()
        {
            if (!_page.ClientScript.IsClientScriptBlockRegistered(GetType(),
                  "DragPanel_$$_Ex_Identity_For_ColumnDrag"))
            {
                _page.Form.Controls.Add(new LiteralControl(
"<div id=\"dragPanel_$$_For_Drag_Control\" "+
"style=\"position:absolute; border-right: #ff3399 1px solid; "+
"border-top: #ff3399 1px solid; border-left: #ff3399 1px solid; color: white; "+
"border-bottom: #ff3399 1px solid; background-color:"+
"darkblue;filter:Alpha(Opacity=85);opacity:0.85;display:none\"></div>"));
                _page.Form.Controls.Add(new LiteralControl(
"<input type=\"hidden\" id=\"dragTreeView_$$_source\" name=\"dragTreeView_$$_source\"/>"));
                _page.Form.Controls.Add(new LiteralControl(
"<input type=\"hidden\" id=\"dragTreeView_$$_target\" name=\"dragTreeView_$$_target\"/>"));
                _page.ClientScript.RegisterClientScriptBlock(GetType(),
                     "DragPanel_$$_Ex_Identity_For_ColumnDrag", " ", true);
            }
        }
 
        public void RegisterEventValidation()
        {
            _page.ClientScript.RegisterForEventValidation(_view.UniqueID, "NodeExchange");
        }
    }
}
pasp_aspnet_drag_manager.js
if(!window.Orphean)
   window.Orphean = {};
if(!window.Orphean.Framework)
   window.Orphean.Framework = {};
if(!window.Orphean.Framework.Web)
   window.Orphean.Framework.Web = {};
if(!window.Orphean.Framework.Web.DragManager)
   window.Orphean.Framework.Web.DragManager = {};
 
Orphean.Framework.Web.DragManager.currentPanel = null;
Orphean.Framework.Web.DragManager.lastMove = null;
Orphean.Framework.Web.DragManager.DragControls = new Array();
 
createDelegate = function(instance, method) {
    return function() {
        method.apply(instance, arguments);
    }
}
 
function getInternetExplorerVersion()
{
    var rv = -1;
    if (navigator.appName == 'Microsoft Internet Explorer') {
        var ua = navigator.userAgent;
        var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
        if (re.exec(ua) != null)
            rv = parseFloat(RegExp.$1);
    }
    return rv;
}
 
 
function Orphean$Framework$Web$DragManager$LastMove() {
    Orphean.Framework.Web.DragManager.lastMove = event.srcElement;
}
 
function Orphean$Framework$Web$DragManager$LastMoveNS(evt) {
    Orphean.Framework.Web.DragManager.lastMove = evt.target;
}
 
function Orphean$Framework$Web$DragManager$Panel_stopie_Proxy()
{
   if (Orphean.Framework.Web.DragManager.currentPanel != null) {
    Orphean.Framework.Web.DragManager.currentPanel.DisplayContent(false);   
    if (navigator.userAgent.indexOf(' Firefox/') == -1)
        document.onselectstart = Orphean.Framework.Web.DragManager.currentPanel.old_select_func;
    if (getInternetExplorerVersion() >= 8 || (navigator.userAgent.indexOf(' Firefox/') != -1))    
    {
        Orphean$Framework$Web$DragManagerMoveNodeIE8();
    }
    Orphean.Framework.Web.DragManager.currentPanel.dragapproved=false;
    Orphean.Framework.Web.DragManager.currentPanel.panelCtrl.innerHTML = '';
    Orphean.Framework.Web.DragManager.currentPanel.stop();
 }
}
 
function Orphean$Framework$Web$DragManager$Panel_drag_dropie_Proxy(evt)
{
 if(Orphean.Framework.Web.DragManager.currentPanel != null)
 {  
    if (navigator.userAgent.indexOf(' Firefox/') > -1)
        Orphean.Framework.Web.DragManager.currentPanel.drag_dropns(evt);
    else
        Orphean.Framework.Web.DragManager.currentPanel.drag_dropie();
 }
}
 
function Orphean$Framework$Web$DragManager$EnableDrag(dragType,enable)
{
   for(var i = 0; i < Orphean.Framework.Web.DragManager.DragControls.length; i++)
   {
      if(Orphean.Framework.Web.DragManager.DragControls[i].dragType == dragType)
         Orphean.Framework.Web.DragManager.DragControls[i].enableDrag = enable;
   }
}
 
function Orphean$Framework$Web$DragManager$IsDragEnable(dragType)
{
   for(var i = 0; i < Orphean.Framework.Web.DragManager.DragControls.length; i++)
   {
      if(Orphean.Framework.Web.DragManager.DragControls[i].dragType == dragType)
         return Orphean.Framework.Web.DragManager.DragControls[i].enableDrag;
   }
 
}
 
function Orphean$Framework$Web$DragManagerMoveNode() {
    if (Orphean.Framework.Web.DragManager.currentPanel != null) {
        document.getElementById("dragTreeView_$$_source").value = Orphean.Framework.Web.DragManager.currentPanel.containerCtrl.id;
        document.getElementById("dragTreeView_$$_target").value = event.srcElement.id;
        __doPostBack(Orphean.Framework.Web.DragManager.currentPanel.postBackOwner,
                        "NodeExchange");
        Orphean$Framework$Web$DragManager$Panel_stopie_Proxy();
    }
 
}
 
function Orphean$Framework$Web$DragManagerMoveNodeIE8() {
    if (Orphean.Framework.Web.DragManager.currentPanel != null && Orphean.Framework.Web.DragManager.lastMove != null) {
 
        document.getElementById("dragTreeView_$$_source").value =
             Orphean.Framework.Web.DragManager.currentPanel.containerCtrl.id;
        document.getElementById("dragTreeView_$$_target").value =
             Orphean.Framework.Web.DragManager.lastMove.id;
        __doPostBack(Orphean.Framework.Web.DragManager.currentPanel.postBackOwner,
                     "NodeExchange");
        Orphean.Framework.Web.DragManager.lastMove = null;
    }
}
 
Orphean.Framework.Web.DragManager.DragControl = function(postBackOwner, ctrl, ctrl1,
                                   dragType, disableVectDrag) {
    this.dragswitch = 0;
    this.nsx;
    this.nsy;
    this.nstemp;
    this.panelCtrl = ctrl;
    this.containerCtrl = ctrl1;
    this.dragapproved = false;
    this.old_select_func = document.onselectstart;
    this.enableDrag = true;
    this.disableVectDrag = disableVectDrag;
    this.dragType = dragType;
    this.postBackOwner = postBackOwner;
    Orphean.Framework.Web.DragManager.DragControls.length++;
    Orphean.Framework.Web.DragManager.DragControls[
         Orphean.Framework.Web.DragManager.DragControls.length - 1] = this;
 
    this.stop = function() {
        Orphean.Framework.Web.DragManager.currentPanel = null;
        document.onmouseup = null;
        document.onmousemove = null;
        if (navigator.userAgent.indexOf(' Firefox/') > -1)
            window.releaseEvents(Event.MOUSEMOVE | Event.MOUSEUP);
    }
 
    this.drag_dropie = function() {
        if (this.dragapproved == true) {
            this.panelCtrl.style.pixelLeft = (tempx + event.clientX +             
                   document.documentElement.scrollLeft +
                   document.body.scrollLeft) - iex;
            if (!this.disableVectDrag)
                this.panelCtrl.style.pixelTop = (tempy + event.clientY +
                   document.documentElement.scrollTop +
                         document.body.scrollTop) - iey;   //disable vect-move
        }
        return false;
    }
 
    this.drag_dropns = function(evt) {
        this.panelCtrl.style.left =
                (tempx + evt.clientX + window.scrollX) - iex + 20 + "px";
        if (!this.disableVectDrag)
            this.panelCtrl.style.top =
                 (tempy + evt.clientY + window.scrollY) - iey + "px";
        return false;
    }
 
    this.initializedragie = function() {
        if (!this.enableDrag) return;
        iex = event.clientX;
        iey = event.clientY;
        tempx = event.clientX;
        tempy = event.clientY;
        this.panelCtrl.style.position = 'absolute';
        this.panelCtrl.style.pixelLeft =
                (tempx + document.documentElement.scrollLeft + document.body.scrollLeft);
        this.panelCtrl.style.pixelTop =
                (tempy + document.documentElement.scrollTop + document.body.scrollTop) - 5;
        Orphean.Framework.Web.DragManager.currentPanel = this;
        this.dragapproved = true;
        this.panelCtrl.innerHTML = this.containerCtrl.innerHTML;
        document.onmousemove = Orphean$Framework$Web$DragManager$Panel_drag_dropie_Proxy;
        this.panelCtrl.onmouseup = Orphean$Framework$Web$DragManager$Panel_stopie_Proxy;
        document.onmouseup = Orphean$Framework$Web$DragManager$Panel_stopie_Proxy;
        this.DisplayContent(true);
        document.onselectstart = function() { return false; } // ie
    }
 
    this.initializedragns = function(evt) {
        if (!this.enableDrag) return;
        iex = evt.clientX;
        iey = evt.clientY;
        tempx = iex;
        tempy = iey;
        this.panelCtrl.style.position = 'absolute';
        this.panelCtrl.style.left = window.scrollX + tempx + 20 + "px";
        this.panelCtrl.style.top = (window.scrollY + tempy) - 5 + "px";
        Orphean.Framework.Web.DragManager.currentPanel = this;
        this.dragapproved = true;
        this.panelCtrl.innerHTML = this.containerCtrl.innerHTML;
        window.captureEvents(Event.MOUSEMOVE | Event.MOUSEUP);
        document.onmousemove = Orphean$Framework$Web$DragManager$Panel_drag_dropie_Proxy;
        this.panelCtrl.onmouseup = Orphean$Framework$Web$DragManager$Panel_stopie_Proxy;
        document.onmouseup = Orphean$Framework$Web$DragManager$Panel_stopie_Proxy;
        this.DisplayContent(true);
        evt.preventDefault(); //Firefox
    }
 
    this.DisplayContent = function(display) {
        if (display)
            this.panelCtrl.style.display = "block";
        else
            this.panelCtrl.style.display = "none";
    }
}
 
 
 
第一個應用例Full Drag-able TreeView
  
接下來是展示區囉,這是你所看到第二段影片的程式碼。 
  
Default.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs"   
           Inherits="WebApplication6._Default" %>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
   
        <asp:TreeView ID="TreeView2" runat="server" Width="291px">
            <Nodes>
                <asp:TreeNode Text="總務處" Value="New Node">
                    <asp:TreeNode Text="會計部" Value="T111">
                        <asp:TreeNode Text="黃一" Value="New Node555"></asp:TreeNode>
                        <asp:TreeNode Text="黃二" Value="黃二"></asp:TreeNode>
                    </asp:TreeNode>
                    <asp:TreeNode Text="收發部" Value="收發部">
                        <asp:TreeNode Text="林1" Value="林1"></asp:TreeNode>
                        <asp:TreeNode Text="林2" Value="林2"></asp:TreeNode>
                    </asp:TreeNode>
                </asp:TreeNode>
                <asp:TreeNode Text="業務處" Value="New Node1">
                    <asp:TreeNode Text="國外部" Value="T333">
                        <asp:TreeNode Text="東1" Value="666654"></asp:TreeNode>
                        <asp:TreeNode Text="東2" Value="東2"></asp:TreeNode>
                    </asp:TreeNode>
                    <asp:TreeNode Text="國內部" Value="國內部">
                        <asp:TreeNode Text="南1" Value="南1"></asp:TreeNode>
                        <asp:TreeNode Text="南2" Value="南2"></asp:TreeNode>
                    </asp:TreeNode>
                </asp:TreeNode>
                <asp:TreeNode Text="客服處" Value="New Node2">
                    <asp:TreeNode Text="線上客服部" Value="線上客服部">
                        <asp:TreeNode Text="Tom1" Value="Tom1"></asp:TreeNode>
                        <asp:TreeNode Text="Tom2" Value="Tom2"></asp:TreeNode>
                    </asp:TreeNode>
                    <asp:TreeNode Text="VIP客服部" Value="VIP客服部">
                        <asp:TreeNode Text="LO1" Value="LO1"></asp:TreeNode>
                        <asp:TreeNode Text="LO2" Value="LO2"></asp:TreeNode>
                    </asp:TreeNode>
                </asp:TreeNode>
            </Nodes>
        </asp:TreeView>
   
        <br />
   
    </div>
    </form>
</body>
</html>
 
Default.aspx.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Reflection;
using System.Security.Permissions;
using System.Collections;
 
namespace WebApplication6
{
    public partial class _Default : System.Web.UI.Page
    {
        private TreeViewDragExtender _viewExtender = null;
 
        protected void Page_Init(object sender, EventArgs e)
        {
            _viewExtender = new TreeViewDragExtender(this, TreeView2);
            _viewExtender.Changing += new NodeChangingHandler(_viewExtender_Changing);
        }
 
        void _viewExtender_Changing(object sender, NodeChangingEventArgs args)
        {
            if(args.SourceNode != args.DestinationNode &&
                args.SourceNode.Parent != args.DestinationNode.Parent)
              args.DestinationNode.ChildNodes.Add(args.SourceNode);
        }
 
        protected override void Render(HtmlTextWriter writer)
        {        
            _viewExtender.RegisterEventValidation();
            base.Render(writer);           
        }
 
        protected void Page_Load(object sender, EventArgs e)
        {           
            //in here, all node is changing.
        }
 
        protected void Page_LoadComplete(object sender, EventArgs e)
        {
            //in here, all node is changed.
        }
       
    }
}
 
 
 
第二個應用例Single Depth Drag-able TreeView
 
接著是第一段影片,僅有最末端可移位的程式碼,其實只有 Changing 事件處不同。
  
Default.aspx.cs
void _viewExtender_Changing(object sender, NodeChangingEventArgs args)
{
            if (args.SourceNode.Depth == 2 && args.DestinationNode.Depth == 1)
            {
                if(args.SourceNode != args.DestinationNode &&
                          args.SourceNode.Parent != args.DestinationNode)
                    args.DestinationNode.ChildNodes.Add(args.SourceNode);
            }
}
 
 
 
第三個應用例:Build From DB, and Support Drag-able TreeView
 
最後一個例子展示如何結合資料庫。 
  
DBTreeViewDemo.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="DBTreeViewDemo.aspx.cs"
          Inherits="WebApplication6.DBTreeViewDemo" %>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
   
        <asp:TreeView ID="TreeView1" runat="server"
            style="margin-right: 172px; margin-bottom: 91px" Width="202px">
        </asp:TreeView>
   
    </div>
    </form>
</body>
</html>
 
DBTreeViewDemo.aspx.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Data;
using System.Data.SqlClient;
 
namespace WebApplication6
{
    public partial class DBTreeViewDemo : System.Web.UI.Page
    {
 
        private TreeViewDragExtender _extender = null;
 
        private void LoadDataToTreeView()
        {
            using (SqlConnection conn =
                new SqlConnection(
@"Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\Database1.mdf; " +
                   "Integrated Security=True;Connect Timeout=30;User Instance=True"))
            {
                SqlDataAdapter adapter = new SqlDataAdapter("SELECT * FROM Nodes", conn);
                DataTable table = new DataTable();
                adapter.Fill(table);
                DataRow[] rows = table.Select("PARENT_ID is NULL");
                LoadNode(TreeView1.Nodes, rows);
            }
        }
 
        private void LoadNode(TreeNodeCollection parent, DataRow[] rows)
        {
            foreach (DataRow row in rows)
            {
                TreeNode newNode = new TreeNode((string)row["NAME"], (string)row["ID"]);
                parent.Add(newNode);
                DataRow[] childRows = row.Table.Select(
                   string.Format("PARENT_ID = '{0}'", (string)row["ID"]));
                if (childRows.Length != 0)
                    LoadNode(newNode.ChildNodes, childRows);
            }
        }
 
        private void UpdateNode(TreeNode sourceNode, TreeNode destinationNode)
        {
            using (SqlConnection conn =
                new SqlConnection(
@"Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\Database1.mdf; " +
           "Integrated Security=True;Connect Timeout=30;User Instance=True"))
            {
                SqlCommand cmd = new SqlCommand(
                  "UPDATE Nodes SET PARENT_ID = @PARENT_ID WHERE ID = @ID", conn);
                cmd.Parameters.AddWithValue("@PARENT_ID", destinationNode.Value);
                cmd.Parameters.AddWithValue("@ID", sourceNode.Value);
                conn.Open();
                cmd.ExecuteNonQuery();
            }
        }
 
        protected void Page_Init(object sender, EventArgs e)
        {
            _extender = new TreeViewDragExtender(this, TreeView1);
            _extender.Changing += new NodeChangingHandler(_extender_Changing);
        }
 
        void _extender_Changing(object sender, NodeChangingEventArgs args)
        {
            if (args.SourceNode.Depth == 2 && args.DestinationNode.Depth == 1)
            {
                if (args.SourceNode != args.DestinationNode &&
                         args.SourceNode.Parent != args.DestinationNode)
                {
                    UpdateNode(args.SourceNode, args.DestinationNode);
                    args.DestinationNode.ChildNodes.Add(args.SourceNode);
                }
            }
        }
 
        protected override void Render(HtmlTextWriter writer)
        {
            _extender.RegisterEventValidation();
            base.Render(writer);
        }
 
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
                LoadDataToTreeView();
        }
    }
}
 
 
後記
 
整個 TreeView 拖曳核心就在 .js TreeViewDragExtender 中,如你所見,一旦你能夠掛上事件至 Node ,那麼 TreeView 就在你的掌握下了。
 
 
 
範例下載(C#)
 
 
範例下載(VB.NET)