Index: Core/Common/src/Core.Common.Controls.TreeView/Core.Common.Controls.TreeView.csproj =================================================================== diff -u -r2750ecd900b9e34ba54bb96d233c464b82d7684c -r18b56bf2778081b817fed8e78925e955e5ffc523 --- Core/Common/src/Core.Common.Controls.TreeView/Core.Common.Controls.TreeView.csproj (.../Core.Common.Controls.TreeView.csproj) (revision 2750ecd900b9e34ba54bb96d233c464b82d7684c) +++ Core/Common/src/Core.Common.Controls.TreeView/Core.Common.Controls.TreeView.csproj (.../Core.Common.Controls.TreeView.csproj) (revision 18b56bf2778081b817fed8e78925e955e5ffc523) @@ -57,6 +57,12 @@ Component + + UserControl + + + TreeViewControl.cs + @@ -91,6 +97,9 @@ TreeView.cs + + TreeViewControl.cs + Index: Core/Common/src/Core.Common.Controls.TreeView/TreeViewControl.Designer.cs =================================================================== diff -u --- Core/Common/src/Core.Common.Controls.TreeView/TreeViewControl.Designer.cs (revision 0) +++ Core/Common/src/Core.Common.Controls.TreeView/TreeViewControl.Designer.cs (revision 18b56bf2778081b817fed8e78925e955e5ffc523) @@ -0,0 +1,57 @@ +namespace Core.Common.Controls.TreeView +{ + partial class TreeViewControl + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.treeView = new System.Windows.Forms.TreeView(); + this.SuspendLayout(); + // + // treeView + // + this.treeView.Dock = System.Windows.Forms.DockStyle.Fill; + this.treeView.Location = new System.Drawing.Point(0, 0); + this.treeView.Name = "treeView"; + this.treeView.Size = new System.Drawing.Size(338, 370); + this.treeView.TabIndex = 0; + // + // TreeViewControl + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.treeView); + this.Name = "TreeViewControl"; + this.Size = new System.Drawing.Size(338, 370); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.TreeView treeView; + } +} Index: Core/Common/src/Core.Common.Controls.TreeView/TreeViewControl.cs =================================================================== diff -u --- Core/Common/src/Core.Common.Controls.TreeView/TreeViewControl.cs (revision 0) +++ Core/Common/src/Core.Common.Controls.TreeView/TreeViewControl.cs (revision 18b56bf2778081b817fed8e78925e955e5ffc523) @@ -0,0 +1,881 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Security.Cryptography; +using System.Windows.Forms; +using Core.Common.Base; +using Core.Common.Controls.TreeView.Properties; +using log4net; +using BaseResources = Core.Common.Base.Properties.Resources; + +namespace Core.Common.Controls.TreeView +{ + public partial class TreeViewControl : UserControl + { + private readonly ICollection treeNodeInfos = new HashSet(); + private readonly Dictionary tagTypeTreeNodeInfoLookup = new Dictionary(); + private readonly int maximumTextLength = 259; + private readonly Dictionary treeNodeObserverLookup = new Dictionary(); + private object data; + private int dropAtLocation; + private Point lastDragOverPoint; + private PlaceholderLocation lastPlaceholderLocation; + private TreeNode nodeDropTarget; + private TreeNode lastPlaceholderNode; + private Graphics placeHolderGraphics; + + private static readonly ILog Log = LogManager.GetLogger(typeof(TreeViewControl)); + + public event EventHandler TreeNodeDoubleClick; + public event EventHandler NodeUpdated; // TODO; Way to explicit! + public event EventHandler NodeDataDeleted; // TODO; Way to explicit! + + public TreeViewControl() + { + InitializeComponent(); + + treeView.ImageList = new ImageList + { + ColorDepth = ColorDepth.Depth32Bit + }; + + treeView.BeforeLabelEdit += TreeViewBeforeLabelEdit; + treeView.AfterLabelEdit += TreeViewAfterLabelEdit; + treeView.AfterCheck += TreeViewAfterCheck; + treeView.KeyDown += TreeViewKeyDown; + treeView.MouseClick += TreeViewMouseClick; + treeView.DoubleClick += TreeViewDoubleClick; + treeView.DragDrop += TreeViewDragDrop; + treeView.DragOver += TreeViewDragOver; + treeView.ItemDrag += TreeViewItemDrag; + treeView.DragLeave += TreeViewDragLeave; + } + + /// + /// Gets or sets the data to render in the tree view. + /// + public object Data + { + get + { + return data; + } + set + { + RemoveAllNodes(); + + data = value; + + if (data == null) + { + return; + } + + AddRootNode(); + + treeView.SelectedNode = treeView.Nodes.Count > 0 ? treeView.Nodes[0] : null; + } + } + + public IEnumerable TreeNodeInfos + { + get + { + return treeNodeInfos; + } + } + + /// + /// This method registers the provided . + /// + /// The to register. + public void RegisterTreeNodeInfo(TreeNodeInfo treeNodeInfo) + { + treeNodeInfos.Add(treeNodeInfo); + tagTypeTreeNodeInfoLookup[treeNodeInfo.TagType] = treeNodeInfo; + } + + public void DeleteNode(TreeNode selectedNode, TreeNodeInfo treeNodeInfo) + { + var message = string.Format(Resources.TreeView_DeleteNodeData_Are_you_sure_you_want_to_delete_the_following_item_0_, selectedNode.Text); + if (MessageBox.Show(message, BaseResources.Confirm, MessageBoxButtons.OKCancel) != DialogResult.OK) + { + return; + } + + if (treeNodeInfo.OnNodeRemoved != null) + { + treeNodeInfo.OnNodeRemoved(selectedNode.Tag, selectedNode.Parent != null ? selectedNode.Parent.Tag : null); + } + + OnNodeDataDeleted(selectedNode); + } + + public void CollapseAll(TreeNode node) + { + node.Collapse(); + + foreach (var childNode in node.Nodes.OfType()) + { + CollapseAll(childNode); + } + } + + public void ExpandAll(TreeNode node) + { + node.Expand(); + + foreach (var childNode in node.Nodes.OfType()) + { + ExpandAll(childNode); + } + } + + /// + /// This method searches all nodes in the for a node with a matching tag. + /// + /// The node data to search the corresponding for. + /// The corresponding the provided node data or null if not found. + public TreeNode GetNodeByTag(object nodeData) + { + return treeView.Nodes.Count > 0 ? GetNodeByTag(treeView.Nodes[0], nodeData) : null; + } + + private static TreeNode GetNodeByTag(TreeNode rootNode, object tag) + { + if (Equals(rootNode.Tag, tag)) + { + return rootNode; + } + + return rootNode.Nodes + .OfType() + .Select(n => GetNodeByTag(n, tag)) + .FirstOrDefault(node => node != null); + } + + /// + /// This method updates the provided . + /// + /// The to update. + /// Thrown when no corresponding can be found for the provided . + private void UpdateNode(TreeNode treeNode) + { + var treeNodeInfo = GetTreeNodeInfoForData(treeNode.Tag); + if (treeNodeInfo == null) + { + throw new InvalidOperationException("No tree node info registered"); + } + + // First of all refresh the child nodes as the other logic below might depend on the presence of child nodes + RefreshChildNodes(treeNode, treeNodeInfo); + + if (treeNodeInfo.Text != null) + { + var text = treeNodeInfo.Text(treeNode.Tag); + + // Having very big strings causes rendering problems in the tree view + treeNode.Text = text.Length > maximumTextLength + ? text.Substring(0, maximumTextLength) + : text; + } + else + { + treeNode.Text = ""; + } + + treeNode.ForeColor = treeNodeInfo.ForeColor != null + ? treeNodeInfo.ForeColor(treeNode.Tag) + : Color.FromKnownColor(KnownColor.ControlText); + SetTreeNodeImageKey(treeNode, treeNodeInfo); + + if (treeNodeInfo.CanCheck != null && treeNodeInfo.CanCheck(treeNode.Tag) + && treeNodeInfo.IsChecked != null) + { + if (treeNode.Checked != treeNodeInfo.IsChecked(treeNode.Tag)) + { + treeView.AfterCheck -= TreeViewAfterCheck; + treeNode.Checked = !treeNode.Checked; + treeView.AfterCheck += TreeViewAfterCheck; + } + treeNode.StateImageIndex = treeNode.Checked ? 1 : 0; + } + + OnNodeUpdated(treeNode); + } + + private void SetTreeNodeImageKey(TreeNode treeNode, TreeNodeInfo treeNodeInfo) + { + if (treeNodeInfo.Image != null) + { + var image = treeNodeInfo.Image(treeNode.Tag); + var imageCollection = treeView.ImageList.Images; + var imageKey = GetImageHash(image); + if (imageCollection.ContainsKey(imageKey)) + { + treeNode.ImageKey = imageKey; + treeNode.SelectedImageKey = imageKey; + } + else + { + treeNode.ImageKey = imageKey; + treeNode.SelectedImageKey = imageKey; + imageCollection.Add(imageKey, image); + } + } + } + + private string GetImageHash(Image image) + { + var stream = new System.IO.MemoryStream(); + image.Save(stream, image.RawFormat); + MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider(); + byte[] hash = md5.ComputeHash(stream.ToArray()); + return System.Text.Encoding.UTF8.GetString(hash); + } + + private void AddRootNode() + { + var rootNode = new TreeNode + { + Tag = data + }; + + UpdateNode(rootNode); + + if (rootNode.Nodes.Count > 0) + { + rootNode.Expand(); + } + + treeView.Nodes.Add(rootNode); + + treeNodeObserverLookup.Add(rootNode, new TreeNodeObserver(rootNode, this)); + } + + private TreeNode CreateTreeNode(TreeNode parentNode, object nodeData) + { + var newNode = new TreeNode + { + Tag = nodeData + }; + + if (treeView.CheckBoxes) + { + newNode.Checked = parentNode.Checked; + } + + UpdateNode(newNode); + + treeNodeObserverLookup.Add(newNode, new TreeNodeObserver(newNode, this)); + + return newNode; + } + + private void RemoveAllNodes() + { + foreach (var treeNode in treeNodeObserverLookup.Keys) + { + treeNodeObserverLookup[treeNode].Dispose(); + } + + treeNodeObserverLookup.Clear(); + treeView.Nodes.Clear(); + } + + private void RemoveTreeNodeFromLookupRecursively(TreeNode treeNode) + { + treeNodeObserverLookup[treeNode].Dispose(); + treeNodeObserverLookup.Remove(treeNode); + + foreach (var childNode in treeNode.Nodes.OfType()) + { + RemoveTreeNodeFromLookupRecursively(childNode); + } + } + + /// + /// This method tries to return a object corresponding to the provided data. + /// + /// The data to find the corresponding for. + /// The for the provided data or null if no corresponding was found. + private TreeNodeInfo GetTreeNodeInfoForData(object item) + { + if (item == null) + { + return null; + } + + TreeNodeInfo treeNodeInfo; + + // Try to find an exact match + tagTypeTreeNodeInfoLookup.TryGetValue(item.GetType(), out treeNodeInfo); + + // Try to match based on class hierarchy + return treeNodeInfo ?? tagTypeTreeNodeInfoLookup.FirstOrDefault(kvp => kvp.Key.IsInstanceOfType(item)).Value; + } + + private void RefreshChildNodes(TreeNode treeNode, TreeNodeInfo treeNodeInfo) + { + var newTreeNodes = new Dictionary(); + var outdatedTreeNodes = treeNode.Nodes.OfType().ToList(); + var currentTreeNodesPerTag = treeNode.Nodes.OfType().ToList().ToDictionary(ctn => ctn.Tag, ctn => ctn); + var newChildNodeObjects = treeNodeInfo.ChildNodeObjects != null + ? treeNodeInfo.ChildNodeObjects(treeNode.Tag) + : new object[0]; + + // Create a list of outdated tree nodes and new tree nodes + for (var i = 0; i < newChildNodeObjects.Length; i++) + { + if (currentTreeNodesPerTag.ContainsKey(newChildNodeObjects[i])) + { + // Remove any node from the list of outdated nodes that should remain part of the node collection + outdatedTreeNodes.Remove(currentTreeNodesPerTag[newChildNodeObjects[i]]); + } + else + { + // If there's no existing node yet, create a new one and add it to the list of new nodes + newTreeNodes.Add(i, CreateTreeNode(treeNode, newChildNodeObjects[i])); + } + } + + // Remove any outdated nodes + foreach (var removedNode in outdatedTreeNodes) + { + treeNode.Nodes.Remove(removedNode); + + RemoveTreeNodeFromLookupRecursively(removedNode); + } + + // Insert any new nodes + foreach (var node in newTreeNodes) + { + treeNode.Nodes.Insert(node.Key, node.Value); + } + } + + # region TreeView event handling + + private void TreeViewBeforeLabelEdit(object sender, NodeLabelEditEventArgs e) + { + var treeNodeInfo = GetTreeNodeInfoForData(e.Node.Tag); + if (treeNodeInfo.CanRename == null || !treeNodeInfo.CanRename(e.Node)) + { + e.CancelEdit = true; + } + } + + private void TreeViewAfterLabelEdit(object sender, NodeLabelEditEventArgs e) + { + // Check Label for null as this indicates the node edit was cancelled + if (e.Label == null) + { + return; + } + + var treeNodeInfo = GetTreeNodeInfoForData(e.Node.Tag); + if (treeNodeInfo.OnNodeRenamed != null) + { + treeNodeInfo.OnNodeRenamed(e.Node.Tag, e.Label); + } + } + + private void TreeViewAfterCheck(object sender, TreeViewEventArgs e) + { + var treeNodeInfo = GetTreeNodeInfoForData(e.Node.Tag); + if (treeNodeInfo.OnNodeChecked != null) + { + treeNodeInfo.OnNodeChecked(e.Node); + } + } + + private void TreeViewKeyDown(object sender, KeyEventArgs keyEventArgs) + { + var selectedNode = treeView.SelectedNode; + if (selectedNode == null) + { + return; + } + + switch (keyEventArgs.KeyData) + { + case Keys.F5: // Refresh the selected node in the tree view + { + if (treeView.SelectedNode != null) + { + UpdateNode(treeView.SelectedNode); + } + break; + } + case Keys.F2: // Start editing the label of the selected node + { + selectedNode.BeginEdit(); + break; + } + case Keys.Apps: // If implemented, show the context menu of the selected node + { + var treeNodeInfo = GetTreeNodeInfoForData(selectedNode.Tag); + + // Update the context menu (relevant in case of keyboard navigation in the tree view) + selectedNode.ContextMenuStrip = treeNodeInfo.ContextMenuStrip != null + ? treeNodeInfo.ContextMenuStrip(selectedNode.Tag, selectedNode, treeNodeInfo) + : null; + + if (treeView.ContextMenu != null && selectedNode.ContextMenuStrip != null) + { + var location = selectedNode.Bounds.Location; + location.Offset(0, selectedNode.Bounds.Height); + + selectedNode.ContextMenuStrip.Show(location); + } + break; + } + case Keys.Enter: // Perform the same action as on double click + { + OnTreeNodeDoubleClick(); + + break; + } + case Keys.Delete: // If allowed, delete the selected node + { + var treeNodeInfo = GetTreeNodeInfoForData(selectedNode.Tag); + + if (treeNodeInfo.CanRemove == null || !treeNodeInfo.CanRemove(selectedNode.Tag, selectedNode.Parent != null ? selectedNode.Parent.Tag : null)) + { + MessageBox.Show(Resources.TreeView_DeleteNodeData_The_selected_item_cannot_be_removed, BaseResources.Confirm, MessageBoxButtons.OK); + break; + } + + DeleteNode(selectedNode, treeNodeInfo); + + break; + } + case Keys.Space: // If applicable, change the checked state of the selected node + { + var treeNodeInfo = GetTreeNodeInfoForData(selectedNode.Tag); + if (treeNodeInfo.CanCheck != null && treeNodeInfo.CanCheck(selectedNode.Tag)) + { + selectedNode.Checked = !selectedNode.Checked; + } + + break; + } + } + + if (keyEventArgs.KeyData == (Keys.Control | Keys.Shift | Keys.Right)) // Expand all tree nodes + { + ExpandAll(selectedNode); + selectedNode.EnsureVisible(); + } + + if (keyEventArgs.KeyData == (Keys.Control | Keys.Shift | Keys.Left)) // Collapse all tree nodes + { + CollapseAll(selectedNode); + selectedNode.EnsureVisible(); + } + } + + private void TreeViewMouseClick(object sender, MouseEventArgs e) + { + var point = treeView.PointToClient(Cursor.Position); + var clickedNode = treeView.GetNodeAt(point); + if (clickedNode == null) + { + return; + } + + var treeNodeInfo = GetTreeNodeInfoForData(clickedNode.Tag); + + if ((e.Button & MouseButtons.Right) != 0) + { + treeView.SelectedNode = clickedNode; + + // Update the context menu + clickedNode.ContextMenuStrip = treeNodeInfo.ContextMenuStrip != null + ? treeNodeInfo.ContextMenuStrip(clickedNode.Tag, clickedNode, treeNodeInfo) + : null; + + return; + } + + var isOnCheckBox = IsOnCheckBox(point); + if (treeNodeInfo.CanCheck != null && treeNodeInfo.CanCheck(clickedNode.Tag) && isOnCheckBox) + { + clickedNode.Checked = !clickedNode.Checked; + } + } + + private bool IsOnCheckBox(Point point) + { + return treeView.HitTest(point).Location.ToString() == "StateImage"; + } + + private void TreeViewDoubleClick(object sender, EventArgs e) + { + OnTreeNodeDoubleClick(); + } + + private void OnTreeNodeDoubleClick() + { + if (TreeNodeDoubleClick != null) + { + TreeNodeDoubleClick(treeView.SelectedNode, EventArgs.Empty); + } + } + + private void TreeViewDragDrop(object sender, DragEventArgs e) + { + ClearPlaceHolders(); + + Point point = treeView.PointToClient(new Point(e.X, e.Y)); + var nodeOver = treeView.GetNodeAt(point); + + var nodeDragging = e.Data.GetData(typeof(TreeNode)) as TreeNode; + + if (nodeOver == null || nodeDragging == null) + { + if (nodeOver != null) + { + e.Effect = DragDropEffects.All; + } + + return; + } + + // Handle dragged items which were originally higher up in the tree under the same parent (all indices shift by one) + if (e.Effect.Equals(DragDropEffects.Move) && nodeDragging.Parent == nodeDropTarget && nodeOver.Index > nodeDragging.Index) + { + if (dropAtLocation > 0) + { + dropAtLocation--; + } + } + + // Ensure the drop location is never < 0 + if (dropAtLocation < 0) + { + dropAtLocation = 0; + } + + var treeNodeInfo = GetTreeNodeInfoForData(nodeDropTarget.Tag); + + try + { + var previousParentNode = nodeDragging.Parent; + + previousParentNode.Nodes.Remove(nodeDragging); + nodeDropTarget.Nodes.Insert(dropAtLocation, nodeDragging); + nodeDragging.EnsureVisible(); + + if (treeNodeInfo.OnDrop != null) + { + treeNodeInfo.OnDrop(nodeDragging, previousParentNode, ToDragOperation(e.Effect), dropAtLocation); + } + } + catch (Exception ex) + { + Log.Error(string.Format(Resources.TreeView_TreeViewDragDrop_Error_during_drag_drop_0_, ex.Message)); + } + } + + private void TreeViewDragOver(object sender, DragEventArgs e) + { + if (lastDragOverPoint.X == e.X && lastDragOverPoint.Y == e.Y) + { + return; + } + + lastDragOverPoint = new Point(e.X, e.Y); + + var point = treeView.PointToClient(lastDragOverPoint); + var nodeOver = treeView.GetNodeAt(point); + var nodeDragging = e.Data.GetData(typeof(TreeNode)) as TreeNode; + + if (nodeOver == null || nodeDragging == null || nodeOver == nodeDragging || IsChildOf(nodeOver, nodeDragging)) + { + ClearPlaceHolders(); + + return; + } + + ScrollIntoView(point, nodeOver, sender); + + PlaceholderLocation placeholderLocation = GetPlaceHoldersLocation(nodeDragging, nodeOver, e); + + if (null == nodeDropTarget) + { + return; + } + + var treeNodeInfo = GetTreeNodeInfoForData(nodeDropTarget.Tag); + + DragOperations allowedOperations = treeNodeInfo.CanDrop != null + ? treeNodeInfo.CanDrop(nodeDragging, nodeDropTarget, ToDragOperation(e.AllowedEffect)) + : DragOperations.None; + + e.Effect = ToDragDropEffects(allowedOperations); + + if (PlaceholderLocation.None == placeholderLocation) + { + return; + } + + // Determine whether ot not the node can be dropped based on the allowed operations. + // A node can also be a valid drop traget if it is the root item (nodeDragging.Parent == null). + var dragOperations = treeNodeInfo.CanDrop != null + ? treeNodeInfo.CanDrop(nodeDragging, nodeDropTarget, allowedOperations) + : DragOperations.None; + + if (DragOperations.None != dragOperations) + { + DrawPlaceholder(nodeOver, placeholderLocation); + } + else + { + ClearPlaceHolders(); + + e.Effect = DragDropEffects.None; + } + } + + private void TreeViewItemDrag(object sender, ItemDragEventArgs e) + { + // gather allowed effects for the current item. + var sourceNode = (TreeNode)e.Item; + var treeNodeInfo = GetTreeNodeInfoForData(sourceNode.Tag); + + DragOperations dragOperation = treeNodeInfo.CanDrag != null + ? treeNodeInfo.CanDrag(sourceNode.Tag, sourceNode) + : DragOperations.None; + + DragDropEffects effects = ToDragDropEffects(dragOperation); + + if (effects == DragDropEffects.None) + { + return; + } + + // store both treenode and tag of treenode in dataobject + // to be dragged. + IDataObject dataObject = new DataObject(); + dataObject.SetData(typeof(TreeNode), sourceNode); + + if (sourceNode.Tag != null) + { + dataObject.SetData(sourceNode.Tag.GetType(), sourceNode.Tag); + } + + treeView.DoDragDrop(dataObject, effects); + } + + private void TreeViewDragLeave(object sender, EventArgs e) + { + ClearPlaceHolders(); + } + + private void DrawPlaceholder(TreeNode node, PlaceholderLocation location) + { + if (lastPlaceholderNode == node && lastPlaceholderLocation == location) + { + return; + } + + ClearPlaceHolders(); + + lastPlaceholderNode = node; + lastPlaceholderLocation = location; + + placeHolderGraphics = treeView.CreateGraphics(); + node.DrawPlaceHolder(location, treeView.CreateGraphics()); + } + + private void ClearPlaceHolders() + { + if (placeHolderGraphics != null) + { + lastPlaceholderNode = null; + + treeView.Refresh(); + + placeHolderGraphics.Dispose(); + placeHolderGraphics = null; + } + } + + private DragOperations ToDragOperation(DragDropEffects dragDropEffects) + { + return (DragOperations) Enum.Parse(typeof(DragOperations), dragDropEffects.ToString()); + } + + private bool IsChildOf(TreeNode childNode, TreeNode node) + { + while (childNode != null && childNode.Parent != null) + { + if (childNode.Parent.Equals(node)) + { + return true; + } + + childNode = childNode.Parent; // Walk up the tree + } + + return false; + } + + private static void ScrollIntoView(Point point, TreeNode nodeOver, object sender) + { + var treeView = sender as TreeView; + if (treeView == null) + { + return; + } + int delta = treeView.Height - point.Y; + if ((delta < treeView.Height / 2) && (delta > 0)) + { + var nextVisibleNode = nodeOver.NextVisibleNode; + if (nextVisibleNode != null) + { + nextVisibleNode.EnsureVisible(); + } + } + if ((delta > treeView.Height / 2) && (delta < treeView.Height)) + { + var previousVisibleNode = nodeOver.PrevVisibleNode; + if (previousVisibleNode != null) + { + previousVisibleNode.EnsureVisible(); + } + } + } + + private PlaceholderLocation GetPlaceHoldersLocation(TreeNode nodeDragging, TreeNode nodeOver, DragEventArgs e) + { + var loc = PlaceholderLocation.None; + int offsetY = treeView.PointToClient(Cursor.Position).Y - nodeOver.Bounds.Top; + + if (offsetY < nodeOver.Bounds.Height / 3 && nodeDragging.NextNode != nodeOver) + { + if (nodeOver.Parent != null) + { + var treeNodeInfo = GetTreeNodeInfoForData(nodeOver.Parent.Tag); + if (treeNodeInfo.CanInsert != null && treeNodeInfo.CanInsert(nodeDragging, nodeOver)) + { + nodeDropTarget = nodeOver.Parent; + dropAtLocation = nodeOver.Parent == null ? 0 : nodeOver.Parent.Nodes.IndexOf(nodeOver); + loc = PlaceholderLocation.Top; + } + else + { + nodeDropTarget = nodeOver; + dropAtLocation = 0; + loc = PlaceholderLocation.Middle; + } + } + else + { + nodeDropTarget = nodeOver; + dropAtLocation = 0; + loc = PlaceholderLocation.Middle; + } + } + else if ((nodeOver.Parent != null) && (offsetY > nodeOver.Bounds.Height - nodeOver.Bounds.Height / 3) && + nodeDragging.PrevNode != nodeOver) + { + var treeNodeInfo = GetTreeNodeInfoForData(nodeOver.Parent.Tag); + if (treeNodeInfo.CanInsert != null && treeNodeInfo.CanInsert(nodeDragging, nodeOver)) + { + nodeDropTarget = nodeOver.Parent; + dropAtLocation = nodeOver.Parent == null + ? 0 + : nodeOver.Parent.Nodes.IndexOf(nodeOver) + 1; + loc = PlaceholderLocation.Bottom; + } + else + { + nodeDropTarget = nodeOver; + dropAtLocation = 0; + loc = PlaceholderLocation.Middle; + } + } + else if (nodeDragging != nodeOver && offsetY < nodeOver.Bounds.Height - nodeOver.Bounds.Height / 4 + && offsetY > nodeOver.Bounds.Height / 4) + { + nodeDropTarget = nodeOver; + dropAtLocation = 0; + loc = PlaceholderLocation.Middle; + } + + if (loc == PlaceholderLocation.None || + (loc == PlaceholderLocation.Middle && nodeDropTarget == nodeDragging.Parent)) + { + ClearPlaceHolders(); + e.Effect = DragDropEffects.None; + } + return loc; + } + + private DragDropEffects ToDragDropEffects(DragOperations operation) + { + return (DragDropEffects) Enum.Parse(typeof(DragDropEffects), operation.ToString()); + } + + # endregion + + # region Event handling + + private void OnNodeUpdated(TreeNode treeNode) + { + if (NodeUpdated != null) + { + NodeUpdated(treeNode, EventArgs.Empty); + } + } + + private void OnNodeDataDeleted(TreeNode node) + { + if (NodeDataDeleted != null) + { + NodeDataDeleted(this, new TreeNodeDataDeletedEventArgs(node.Tag)); + } + } + + # endregion + + # region Nested types + + private class TreeNodeObserver : IDisposable, IObserver + { + private readonly TreeNode treeNode; + private readonly TreeViewControl treeViewControl; + + public TreeNodeObserver(TreeNode treeNode, TreeViewControl treeViewControl) + { + this.treeNode = treeNode; + this.treeViewControl = treeViewControl; + + var observable = treeNode.Tag as IObservable; + if (observable != null) + { + observable.Attach(this); + } + } + + public void Dispose() + { + var observable = treeNode.Tag as IObservable; + if (observable != null) + { + observable.Detach(this); + } + } + + public void UpdateObserver() + { + treeViewControl.UpdateNode(treeNode); + } + } + + # endregion + } +} Index: Core/Common/src/Core.Common.Controls.TreeView/TreeViewControl.resx =================================================================== diff -u --- Core/Common/src/Core.Common.Controls.TreeView/TreeViewControl.resx (revision 0) +++ Core/Common/src/Core.Common.Controls.TreeView/TreeViewControl.resx (revision 18b56bf2778081b817fed8e78925e955e5ffc523) @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file