• WPF 实现个简单MVVM的TreeComboBox


    树行下拉框,使用UserControl制作,先看下效果图吧

    源码下载:
    https://files-cdn.cnblogs.com/files/wandia/TreeComboBoxDemo.zip

    实现原理:

    通过修改ComboBox中的样式模板,在里面放一个TreeView就是最简单的思路, 这里简单列下修改后ComboBox最简单的控件模板

    <ControlTemplate x:Key="ComboBoxTemplate" TargetType="{x:Type ComboBox}">
        <Grid x:Name="templateRoot" SnapsToDevicePixels="true">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="0"/>
            </Grid.ColumnDefinitions>
            <Popup x:Name="PART_Popup" AllowsTransparency="true" Grid.ColumnSpan="2" Height="auto"
                    IsOpen="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" 
                    Margin="1" PopupAnimation="{DynamicResource {x:Static SystemParameters.ComboBoxPopupAnimationKey}}" 
                    Placement="Bottom">
                  <Border x:Name="shadow">
                    <ScrollViewer>
                       <TreeView x:Name="InnerTreeView"/>
                    </ScrollViewer>
                 </Border>
            </Popup>
            <ToggleButton x:Name="toggleButton" />             
            <ContentPresenter x:Name="contentPresenter" />
        </Grid>
    </ControlTemplate>    
    <Style x:Key="CmbTreeComboBox" TargetType="{x:Type ComboBox}">
       <Setter Property="Template" Value="{StaticResource ComboBoxTemplate}"/>
    <Style/>
    

    自定义用户控件放ComboBox即可

    <UserControl   x:Class="Alarm.Main.Controls.TreeComboBox"
                 d:DesignHeight="30" d:DesignWidth="100">
        <ComboBox Style="{StaticResource CmbTreeComboBox}"  x:Name="ComboBox"/>
    </UserControl>
    
    

    为了达到基本的定制开发定义下最简单所需的依赖属性

    选中值 SelectedItem

            public object SelectedItem
            {
                get { return (object)GetValue(SelectedItemProperty); }
                set { SetValue(SelectedItemProperty, value); }
            }
            public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.Register(
                "SelectedItem", typeof(object), typeof(TreeComboBox),
                new PropertyMetadata(null, OnSelectedItem_Changed));
    

    TreeView所需的数据源 ItemsSource

     public IEnumerable ItemsSource
            {
                get { return (IEnumerable)GetValue(ItemsSourceProperty); }
                set { SetValue(ItemsSourceProperty, value); }
            }
            public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
                "ItemsSource", typeof(IEnumerable), typeof(TreeComboBox),
                new PropertyMetadata(null, OnItemsSource_Changed));
    

    TreeView选中项的呈现字符串值DisplayPath

    TreeView选中TreeViewItem后, 用于在ComboBox显示相应的值,该属性类似下拉框的DisplayMemberPath

     public string DisplayPath
            {
                get { return (string)GetValue(DisplayPathProperty); }
                set { SetValue(DisplayPathProperty, value); }
            }
            public static readonly DependencyProperty DisplayPathProperty = DependencyProperty.Register(
                "DisplayPath", typeof(string), typeof(TreeComboBox),
                new PropertyMetadata("", OnDisplayPath_Changed));
    

    一个Style类型的属性TreeItemStyle用于定制化TreeViewItem中样式

      public Style TreeItemStyle
            {
                get { return (Style)GetValue(TreeItemStyleProperty); }
                set { SetValue(TreeItemStyleProperty, value); }
            }
            public static readonly DependencyProperty TreeItemStyleProperty = DependencyProperty.Register(
                "TreeItemStyle", typeof(Style), typeof(TreeComboBox),
                new PropertyMetadata(null, OnTreeItemStyle_Changed));
    

    一个HierarchicalDataTemplate类型的属性TreeItemItemTemplate用于定制化TreeViewer的子项数据模板

     public HierarchicalDataTemplate TreeItemItemTemplate
            {
                get { return (HierarchicalDataTemplate)GetValue(TreeItemItemTemplateProperty); }
                set { SetValue(TreeItemItemTemplateProperty, value); }
            }
            public static readonly DependencyProperty TreeItemItemTemplateProperty = DependencyProperty.Register(
                "TreeItemItemTemplate", typeof(HierarchicalDataTemplate), typeof(TreeComboBox),
                new PropertyMetadata(null, OnTreeItemItemTemplate_Changed));
    

    增加路由事件和命令满足基本的用法

    定义选中值发生改变事件

     // Register a custom routed event using the Bubble routing strategy.
            public static readonly RoutedEvent SelectedItemChangedEvent = EventManager.RegisterRoutedEvent(
                name: "SelectedItemChanged",
                routingStrategy: RoutingStrategy.Bubble,
                handlerType: typeof(RoutedEventHandler),
                ownerType: typeof(TreeComboBox));
    
            // Provide CLR accessors for assigning an event handler.
            public event RoutedEventHandler SelectedItemChanged
            {
                add { AddHandler(SelectedItemChangedEvent, value); }
                remove { RemoveHandler(SelectedItemChangedEvent, value); }
            }
    

    定义命令实现MVVM

    这里命令由TreeViewItem的点击事件MouseDown触发

            public ICommand TreeViewItemClickCommand
            {
                get { return (ICommand)GetValue(TreeViewItemClickCommandProperty); }
                set { SetValue(TreeViewItemClickCommandProperty, value); }
            }
            public static readonly DependencyProperty TreeViewItemClickCommandProperty =
                DependencyProperty.Register("TreeViewItemClickCommand", typeof(ICommand), typeof(TreeComboBox), new PropertyMetadata(null));
    
    
            public object TreeViewItemClickCommandParameter
            {
                get { return (object)GetValue(TreeViewItemClickCommandParameterProperty); }
                set { SetValue(TreeViewItemClickCommandParameterProperty, value); }
            }
            public static readonly DependencyProperty TreeViewItemClickCommandParameterProperty =
                DependencyProperty.Register("TreeViewItemClickCommandParameter", typeof(object), typeof(TreeComboBox), new PropertyMetadata(null));
    
            public IInputElement TreeViewItemClickTarget
            {
                get { return (IInputElement)GetValue(TreeViewItemClickTargetProperty); }
                set { SetValue(TreeViewItemClickTargetProperty, value); }
            }
            public static readonly DependencyProperty TreeViewItemClickTargetProperty =
                DependencyProperty.Register("TreeViewItemClickTarget", typeof(IInputElement), typeof(TreeComboBox), new PropertyMetadata(null));
            
    

    实现要点

    控件显示Load事件加载

    自定义控件的ComboBox中无法直接获取TreeView, 可以借助Load事件,获取控件可简单通过当前控件.Template.FindName("控件名",当前控件)直接获取
    在Load时对TreeView进行以上定义依赖属性的设置,如 TreeItemItemTemplate、 ItemsSource、 TreeItemStyle 等

      this.Loaded += OnThis_Loaded;
    
    
    
    private void OnThis_Loaded(object sender, RoutedEventArgs e)
    {
        if (DropDownTreeViewer == null)
        {
            DropDownTreeViewer = this.ComboBox.Template.FindName("InnerTreeView", this.ComboBox) as TreeView;
            if (DropDownTreeViewer != null)
            {
                if (DropDownTreeViewer.ItemTemplate != this.TreeItemItemTemplate)
                {
                    DropDownTreeViewer.ItemTemplate = this.TreeItemItemTemplate;
                }
                if (DropDownTreeViewer.ItemsSource != this.ItemsSource)
                {
                    DropDownTreeViewer.Items.Clear();
                    DropDownTreeViewer.ItemsSource = this.ItemsSource;
                }
                if (DropDownTreeViewer.ItemContainerStyle != this.TreeItemStyle)
                {
                    DropDownTreeViewer.ItemContainerStyle = this.TreeItemStyle;
                }
            }
        }
        if (this.SelectedItem != null && string.IsNullOrEmpty(this.ComboBox.Text))
        {
            SetDisplayText(this.SelectedItem);
        }
        else if (this.SelectedItem == null)
        {
            SetDisplayText(null);
        }
    }
    

    SetDisplayText方法

    此方法用于让Combox对SelectedItem显示相应的值,使用反射简单实现

    private void SetDisplayText(object item)
    {
        if (item == null)
        {
            this.ComboBox.Items.Clear();
            this.ComboBox.Text = "";
            return;
        }
        if (!string.IsNullOrEmpty(this.DisplayPath))
        {
            string[] propName = this.DisplayPath.Split('.');
            if (propName.Length > 0)
            {
                object tmpVAlue = item;
                for (int i = 0; i < propName.Length; i++)
                {
                    PropertyInfo property = tmpVAlue.GetType().GetProperty(propName[i], BindingFlags.Instance | BindingFlags.Public);
                    if (property != null)
                    {
                        tmpVAlue = property.GetValue(tmpVAlue);
                    }
                    if (tmpVAlue == null || i == propName.Length - 1)
                    {
                        this.ComboBox.Items.Clear();
                        string value = (tmpVAlue ?? "NaN??").ToString();
                        this.ComboBox.Items.Add(value);
                        this.ComboBox.Text = value;
                        return;
                    }
                }
            }
        }
        this.ComboBox.Items.Clear();
        this.ComboBox.Items.Add(this.SelectedItem.ToString());
        this.ComboBox.Text = this.ComboBox.Items[0].ToString();
    }
    

    下拉时在TreeView中选中相应的项

    TreeView是无法直接对SelectedValue进行赋值,因为只读,此出通过ComboBox的DropDownOpen事件完成,见代码,
    另外TreeView中如果发生选中项ComboBox会自动关闭,这里用开启线程重新触发选中TreeViewItem
    同时,如果选中项在TreeViewItem中也会出现这个问题

    this.ComboBox.DropDownOpened += OnComboBox_DropDownOpened;
    
    
    private void OnComboBox_DropDownOpened(object sender, EventArgs e)
    {
        if (DropDownTreeViewer != null && this.SelectedItem != null)
        {
            if (DropDownTreeViewer.SelectedValue != this.SelectedItem)
            {
                this.SetTreeViewSelectedItem(this.SelectedItem);
            }
        }
    }
    private void SetTreeViewSelectedItem(object selectedValue)
    {
        if (selectedValue == null) return;
        if (this.DropDownTreeViewer != null)
        {
            if (this.DropDownTreeViewer.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
            {
                SelectedTheTreeViewItem(selectedValue);
            }
            else
            {
                EventHandler[] eventHandler = new EventHandler[1];
                eventHandler[0] = new EventHandler((o1, e1) =>
                {
                    if (this.DropDownTreeViewer.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
                    {
                        this.DropDownTreeViewer.ItemContainerGenerator.StatusChanged -= eventHandler[0];
                        SelectedTheTreeViewItem(selectedValue);
                    };
                });
                this.DropDownTreeViewer.ItemContainerGenerator.StatusChanged += eventHandler[0];
            }
        }
    }
    private void SelectedTheTreeViewItem(object selectedValue)
    {
        for (int i = 0; i < this.DropDownTreeViewer.ItemContainerGenerator.Items.Count; i++)
        {
            TreeViewItem treeViewItem = this.DropDownTreeViewer.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
            if (treeViewItem != null)
            {
                object item = this.DropDownTreeViewer.ItemContainerGenerator.ItemFromContainer(treeViewItem);
                if (item == selectedValue)
                {
                    treeViewItem.IsSelected = true;
                    TunThreadSetIsOpen();
                    return;
                }
                else
                {
                    SetTreeViewItemSelected(treeViewItem, selectedValue);
                }
            }
        }
    }
    private void TunThreadSetIsOpen()
    {
        //这个事件实在下拉时候发生的,因为选中了之后会触发下拉框自动回收,这就很操蛋,只能延迟后重新打开
        new System.Threading.Thread(() =>
        {
            System.Threading.Thread.Sleep(100);
            this.Dispatcher.Invoke(() =>
            {
                if (this.ComboBox.IsDropDownOpen == false)
                {
                    this.ComboBox.IsDropDownOpen = true;
                }
            });
        }).Start();
    }
    void SetTreeViewItemSelected(TreeViewItem treeViewItem, object selectedValue)
    {
        if (treeViewItem == null || selectedValue == null) return ;
        if(treeViewItem.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
        {
            for (int i = 0; i < treeViewItem.ItemContainerGenerator.Items.Count; i++)
            {
                TreeViewItem childTreeViewItem = treeViewItem.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
                if (childTreeViewItem != null)
                {
                    object item = treeViewItem.ItemContainerGenerator.ItemFromContainer(childTreeViewItem);
                    if (item == selectedValue)
                    {
                        childTreeViewItem.IsSelected = true;
                        TunThreadSetIsOpen();
                        return;
                    }
                    else
                    {
                            SetTreeViewItemSelected(childTreeViewItem, selectedValue);
                    }
                }
            }
        }
        else
        {
            EventHandler[] eventHandler = new EventHandler[1];
            eventHandler[0] = new EventHandler((o1, e1) =>
            {
                if (treeViewItem.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
                {
                    treeViewItem.ItemContainerGenerator.StatusChanged -= eventHandler[0];
                    SetTreeViewItemSelected(treeViewItem, selectedValue);
                };
            });
            treeViewItem.ItemContainerGenerator.StatusChanged += eventHandler[0];
        }
    }
    

    监听路由事件

    监听TreeView.SelectedItemChanged用于更改ComboBox的显示值

     EventManager.RegisterClassHandler(typeof(TreeView), TreeView.SelectedItemChangedEvent,
                    new RoutedPropertyChangedEventHandler<object>(OnTreeView_SelectedItemChangedEvent));
    
    
    private void OnTreeView_SelectedItemChangedEvent(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        TreeView treeView = sender as TreeView;
        if (treeView != this.DropDownTreeViewer) return;
        if (e.NewValue != null)
        {
            this.SelectedItem = e.NewValue;
            SetDisplayText(e.NewValue);
        }
        //写这个线程启动因为选中项之后下拉框不会关闭,如果在选中时关闭,将会重新触发一次此事件,导致选中的值回归选中前
        new System.Threading.Thread(() =>
        {
            System.Threading.Thread.Sleep(50);
            this.Dispatcher.Invoke(() =>
            {
                this.ComboBox.IsDropDownOpen = false;
            });
        }).Start();
    }
    

    监听TreeViewItem.MouseLeftButtonUp用于ComboBox没正确关闭时重新关闭

    EventManager.RegisterClassHandler(typeof(TreeViewItem), TreeViewItem.MouseLeftButtonDownEvent,
        new MouseButtonEventHandler(OnTreeViewItem_MouseDown));
    
    
    private void OnTreeViewItem_MouseUP(object sender, MouseButtonEventArgs e)
    {
        if (this.ComboBox.IsDropDownOpen == true)
        {
            this.ComboBox.IsDropDownOpen = false;
        }
    }
    

    监听TreeViewItem.MouseLeftButtonDown用于触发简单的Command,此时SelectedItem并未马上绑定,这里通过
    通过DataContext将其从CommandParameter传递到命令调用

    EventManager.RegisterClassHandler(typeof(TreeViewItem), TreeViewItem.MouseLeftButtonDownEvent,
        new MouseButtonEventHandler(OnTreeViewItem_MouseDown));
    
    
    private void OnTreeViewItem_MouseDown(object sender, MouseButtonEventArgs e)
    {
        TreeViewItem treeViewItem = sender as TreeViewItem;
        if (treeViewItem != null)
        {
            object commandParameter = TreeViewItemClickCommandParameter;
            if (commandParameter == null)
                commandParameter = treeViewItem.DataContext;
    
            TreeView treeParent = FindVisualParent<TreeView>(treeViewItem);
            if (treeParent != null && treeParent == this.DropDownTreeViewer)
            {
                RoutedCommand command = TreeViewItemClickCommand as RoutedCommand;
                if (command != null)
                    command.Execute(commandParameter, TreeViewItemClickTarget);
                else if (TreeViewItemClickCommand != null)
                    this.TreeViewItemClickCommand.Execute(commandParameter);
            }
        }
    }
    public static T FindVisualParent<T>(DependencyObject obj) where T : DependencyObject
    {
        if (obj == null) return null;
        DependencyObject parent = VisualTreeHelper.GetParent(obj);
        while (parent != null)
        {
            if (parent is T)
                return parent as T;
            else
                parent = VisualTreeHelper.GetParent(parent);
        }
        return parent as T;
    }
    

    用法请见源码

    xaml

    <Controls:TreeComboBox Width="120" Height="30"  VerticalAlignment="Top" HorizontalAlignment="Left"
        TreeItemStyle="{StaticResource TreeViewItemStyle1}"
        ItemsSource="{Binding TreeComboxItemsSource}"
        SelectedItem="{Binding SelectedItem,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
        SelectedItemChanged="TreeComboBox_SelectedItemChanged"
        DisplayPath="Data.StepName"
        TreeViewItemClickCommand="{Binding CMD_TreeViewItemClick}">
        <Controls:TreeComboBox.TreeItemItemTemplate>
            <HierarchicalDataTemplate ItemsSource="{Binding Childeren}">
                <TextBlock Text="{Binding Data.StepName}" />
            </HierarchicalDataTemplate>
        </Controls:TreeComboBox.TreeItemItemTemplate>
    </Controls:TreeComboBox> 
    

    源码下载:
    https://files-cdn.cnblogs.com/files/wandia/TreeComboBoxDemo.zip

  • 相关阅读:
    sed思维导图
    Golang计算文件MD5
    grpc入门
    解决使用fastjson时,null值在转JSONObject时丢失的问题
    ArcGIS空间分析案例教程网格和标记生成
    在 VirtualBox VM 中安装 OpenWRT
    aws中各个系统的用户名
    emacs简单配置和scheme环境
    Vue 中引入 json 的三种方法
    Lodash节流和防抖总结
  • 原文地址:https://www.cnblogs.com/wandia/p/16255759.html
Copyright © 2020-2023  润新知