WPF对绑定到数据的树初始化选择项

【需求】

1——

对一棵绑定到数据源的树,当初始化加载后,设置树的初始化选择对象。

比如:当我们实现对行业的选择树时,希望当打开这棵树的时候,树能自动把之前已经选择的或者系统默认的树标记为选择状态。

2——

数据源是一个动态加载的集合,即:当我们展开或选择某一个数据节点的时候,系统自动实现该数据节点下级子数据集合的加载

如Windows的文件系统,资源管理器不是一次性的把整个文件系统都加载在内存,而是当我们需要打开某一个文件夹的时候,文件系统才会去加载这个文件夹的下级文件信息。

 

【问题】

1——

当TreeViewItem被选择或展开后自动加载下级数据集合需要控制需要额外编写代码实现

2——

当TreeView绑定到数据的时候,封闭了开发人员直接使用TreeViewItem的可能,要操纵TreeViewItem需要通过特殊的手段来实现。

3——

WPF中的TreeView不可以通过设置SelectedItem(只读)来实现当前选择对象的设定。

 

【解决】

1——

可以通过路由事件跟踪,TreeViewItem的SelectedEvent和ExpandedEvent是可路由事件,因此通过在TreeView或其上级容器中跟踪这两个路由事件来达到目的:

 

    protected override void OnInitialized(EventArgs e)
    {
        this.AddHandler(TreeViewItem.ExpandedEvent, new RoutedEventHandler(TreeViewItemHandler));
        this.AddHandler(TreeViewItem.SelectedEvent, new RoutedEventHandler(TreeViewItemHandler));
        base.OnInitialized(e);
    }

    void TreeViewItemHandler(object sender, RoutedEventArgs e)
    {
        TreeViewItemEx item = e.OriginalSource as TreeViewItemEx;
        if (item != null && item.DataContext is FileFolder)
        {
            (item.DataContext as FileFolder).Load();
        }
    }

 

 2——

如问题1的解决方案,当跟踪到TreeViewItem被选择或展开时,获取该TreeViewItem的DataContext(即数据对象)并执行数据的加载:

 

        TreeViewItemEx item = e.OriginalSource as TreeViewItemEx;
        if (item != null && item.DataContext is FileFolder)
        {
            (item.DataContext as FileFolder).Load();
        }

 

3——

因为SelectedItem是只读的,而且,TreeView也没有提供设置初始化选择项的功能,要达到目的就只能自己想办法了。

由于TreeView的树结构中没有提供直接访问树节点对象的支持,我们只能通过其对象集合Items属性来获取数据对象,在查阅相关资料后,找到了通过数据对象来获取对应的TreeViewItem的方法:

 

    public static TreeViewItemEx GetChildItem(ItemsControl container, object data)
    {
        if (container == null || data == null)
        {
            return null;
        }
        try
        {
            TreeViewItemEx result = container.ItemContainerGenerator.ContainerFromItem(data) as TreeViewItemEx;
            if (result != null)
            {
                return result;
            }
        }
        catch
        {
        }
        foreach (var item in container.Items)
        {
            if (item == null)
            {
                continue;
            }
            if (item is TreeViewItemEx && (item as TreeViewItemEx).DataContext != null && (item as TreeViewItemEx).DataContext.Equals(data))
            {
                return item as TreeViewItemEx;
            }
            if (item.Equals(data))
            {
                return container.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItemEx;
            }
        }
        return null;
    }

 

【方案】

1——

要达到指定数据对象对应的TreeViewItem的选择,必须有一条从树的根节点到到目标节点的数据路径,这里定义这个数据路径为:LinkList<object> list,获取这个数据路径的方法为:

 

    public LinkedList<object> GetDataPathFromRoute(object dataItem)
    {
        LinkedList<object> list = new LinkedList<object>();
        this.GetDataPathFromRoute(list, dataItem);
        return list;
    }

    private void GetDataPathFromRoute(LinkedList<object> list, object dataItem)
    {
        if (dataItem == null)
        {
            return;
        }
        object parent = GetParentObject(dataItem);
        if (IsStopObject(dataItem))
        {
            if (dataItem == null)
            {
                return;
            }
            list.AddLast(dataItem);
            return;
        }
        this.GetDataPathFromRoute(list, parent);
        list.AddLast(dataItem);
    }

    private bool IsStopObject(object dataItem)
    {
        if (dataItem == null)
        {
            return true;
        }
        if (dataItem is string && string.IsNullOrEmpty(dataItem as string))
        {
            return true;
        }
        return this.Equals(dataItem);
    }

    private object GetParentObject(object dataItem)
    {
        if (dataItem is FileItemInfoBase)
        {
            return this.GetParentObject((dataItem as FileItemInfoBase).FileItem);
        }
        if (dataItem is FileInfo)
        {
            return (dataItem as FileInfo).Directory;
        }
        if (dataItem is DirectoryInfo)
        {
            if ((dataItem as DirectoryInfo).Root == dataItem)
            {
                return null;
            }
            return (dataItem as DirectoryInfo).Parent;
        }
        if (dataItem is string)
        {
            if (File.Exists(dataItem as string))
            {
                return new FileInfo(dataItem as string).Directory;
            }
            return new DirectoryInfo(dataItem as string).Parent;
        }
        return null;
    }

 

2——

数据路径树节点的展开:

    public object InitializeItem
    {
        set
        {
            LinkedList<object> list = GetDataPathFromRoute(value);
            if (list != null && list.First != null)
            {
                LinkedListNode<object> node = list.First;
                ItemsControl container = treeView;
                while (node != null)
                {
                    TreeViewItemEx item = GetChildItem(container, node);
                    if (item == null)
                    {
                        container = null;
                        return;
                    }
                    item.IsExpanded = true;
                    container = item;
                    node = node.Next;
                }
                if (container != null && container is TreeViewItem)
                {
                    (container as TreeViewItem).IsSelected = true;
                }
            }
        }
    }

在实际的运行中,这段代码并不可行,除非数据树(TreeView)的数据源是初始化全加载的(个人臆测,这个没验证过),因为当我们执行TreeViewItem的展开操作的时候,是正确的执行的下级数据集合的加载,但此时,没有办法通过前面的GetChildItem来获取目标的TreeViewItem对象,返回的始终是空。

经过分析,认为TreeViewItem的子对象的创建有一个延迟(跟初始化数据操作InitializeItem在同一个线程中),从而导致了即便数据加载了,而TreeViewItem的下级UI对象还没有创建而是空的。

 

3——

经过对TreeViewItem的各个事件(以及可重载函数=属性)进行跟踪,发现,当加载TreeViewItem的下级数据的时候,会触发ItemsSourceChanged事件,紧接着,会触发OnRender方法的执行,而且,OnRender方法执行后,下级TreeViewItem对象也就已经创建了,因此代码修改如下:

 

        private void setCurrPath_Click(object sender, RoutedEventArgs e)
        {
            if (Directory.Exists(this.rootPath.Text) && this.Folder != null)
            {
                DirectoryInfo dir = new DirectoryInfo(this.currPath.Text);
                LinkedList<object> list = this.Folder.GetDataPathFromRoute(dir);
                if (list.First != null)
                {
                    this.treeViewEx1.SetInitializeSelectedItem(list.First);
                }
            }
        }

TreeViewEx类的SetInitializeItem方法代码如下:

        public void SetInitializeSelectedItem(LinkedListNode<object> dataLink)
        {
            if (dataLink == null || dataLink.Value == null)
            {
                return;
            }
            TreeViewItemEx item = TreeViewItemEx.GetChildItem(this, dataLink.Value);
            if (item == null && this.Items.Count == 1)
            {
                item = TreeViewItemEx.GetChildItem(TreeViewItemEx.GetChildItem(thisthis.Items[0]), dataLink.Value);
            }
            if (item != null)
            {
                if (dataLink.Next != null && dataLink.Next.Value != null)
                {
                    item.SetInitializeSelectedItem(dataLink.Next);
                }
                else
                {
                    item.IsSelected = true;
                }
            }
        }

TreeViewItemEx对应的代码如下:

        private bool _rendered = false;
        protected override void OnItemsSourceChanged(System.Collections.IEnumerable oldValue, System.Collections.IEnumerable newValue)
        {
            this._rendered = false;
            base.OnItemsSourceChanged(oldValue, newValue);
        }

        protected override void OnRender(DrawingContext drawingContext)
        {
            if (!_rendered)
            {
                this.SetInitializeSelectedItem();
                this._rendered = true;
            }
            base.OnRender(drawingContext);
        }

        protected override void OnSelected(RoutedEventArgs e)
        {
            this.IsExpanded = true;
            base.OnSelected(e);
        }

        private bool _mustInitializeSelectedItem = false;
        private LinkedListNode<object> _initializeSelectedItem = null;
        internal void SetInitializeSelectedItem(LinkedListNode<object> dataLink)
        {
            if (dataLink == null || dataLink.Value == null)
            {
                return;
            }
            this._mustInitializeSelectedItem = true;
            this._initializeSelectedItem = dataLink;
            if (this._rendered)
            {
                this.InitializeSelectedItem(false);
            }
        }

        private void InitializeSelectedItem(bool rendered)
        {
            TreeViewItemEx childItem = GetChildItem(thisthis._initializeSelectedItem.Value);
            if (childItem != null)
            {
                if (!childItem.IsExpanded && this._initializeSelectedItem.Next != null && this._initializeSelectedItem.Next.Value != null)
                {
                    childItem.IsExpanded = true;
                    childItem.SetInitializeSelectedItem(this._initializeSelectedItem.Next);
                }
                else
                {
                    childItem.IsSelected = true;
                }
            }
            else
            {
                if (this.IsExpanded || rendered)
                {
                    this.IsSelected = true;
                }
                else
                {
                    this.IsExpanded = true;
                }
            }
        }

        private void SetInitializeSelectedItem()
        {
            if (!this._mustInitializeSelectedItem)
            {
                return;
            }
            _mustInitializeSelectedItem = false;
            this.InitializeSelectedItem(true);
        }

 

至此,需求已经达到。

 

【存在问题】

在以上的解决方案中,理论上,问题已经圆满的解决(甚至自以为是完美的解决了),但在实际使用中,却出现新的问题:

不稳定,有时能成功展开,达到目的(跟树的路径深度无关,不过,树的路径深度低的话,成功的可能性大)。

附参考代码程序。
/Files/Daview/WpfApplication1.zip


 

posted @ 2011-11-04 09:27 无之无 阅读(...) 评论(...) 编辑 收藏