65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF 多级弹出窗口,绑定到分层树

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (3投票s)

2016年6月17日

CPOL

4分钟阅读

viewsIcon

16236

downloadIcon

329

本文介绍了使用弹出窗口显示分层树所需的最小代码。

引言

本文介绍了使用Popups显示分层树所需的最小代码。

在每个树级别的Popup内部,显示了

  • 绑定对象的详细信息
  • 一个带有ToggleButtons(可勾选)的ListBox,并且会再次出现另一个Popup

背景

在此解决方案中,使用了nuget包 PropertyChanged.Fody

它是使用VisualStudio 2010,.NET Framework 4.0开发的。

Using the Code

首先,模型模拟

    [ImplementPropertyChanged]
    public class DataObject
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public DataObject(int ID)
        {
            this.ID = ID;
            this.Name = "Node " + this.ID;
        }
	}

...以及单个树节点的视图模型

    [ImplementPropertyChanged]
    public class VM_Node
    {
        public bool IsChildNodesLoaded { get; set; }
        public DataObject NodeData { get; set; }
        public ViewableCollection <VM_Node> ChildNodes { get; set ; }

        public VM_Node(DataObject data)
        {
            this.ChildNodes = new ViewableCollection<VM_Node>();
            this.ChildNodes.View.CurrentChanged += new EventHandler(ChildNodes_View_CurrentChanged);
            this.NodeData = data;
        }


        public void LoadChildren()
        {
            if (!this.IsChildNodesLoaded)
            {
                this.ChildNodes.ReplaceItems(DataAccessMockup.GetNodeChildren(this.NodeData.ID)
                       .Select(a => new VM_Node(a)));
                this.IsChildNodesLoaded = true ;
            }
            this.ChildNodes.CurrentItem = null ; // clear selection

        }

        void ChildNodes_View_CurrentChanged(object sender, EventArgs e)
        {
            if (this.ChildNodes.CurrentItem != null)
            {
                this.ChildNodes.CurrentItem.LoadChildren();
            }
        }
   }

子节点是惰性加载的:仅当Popup首次打开时。

ViewableCollection类在本篇文章的“兴趣点”部分进行了介绍。我在这里使用它来更轻松地从视图模型中管理ListBox中的选择:当ChildNodesCurrentItem发生变化时,如果需要,将加载CurrentItem的子项,然后清除子集合中的选择。清除是必要的,因为

  • 默认情况下,当首次加载项时,第一个项被设置为当前项。因此,当父选择改变时,如果不将ChildNodes.CurrentItem设置为null,那么在开始时,从根到叶的所有弹出窗口都将打开。
  • 这确保了每次打开Popup时,都会清除之前的选择。

MainWindow的GUI由一个Button和一个在单击Button时出现的Popup组成。Popup的内容绑定到树的根部

    <Grid>
        <Button Content="Open popup" 
        Command ="{Binding Path=OpenPopup, Mode=OneTime}">
            <Button.CommandParameter>
                <sys:Int32>0</sys:Int32>
            </Button.CommandParameter>
        </Button>
        <Popup  IsOpen="{Binding Path=IsRootPopupOpen, Mode =TwoWay}"
               AllowsTransparency="True" Placement ="MousePoint" 
               StaysOpen="False" PopupAnimation="Fade">
            <Popup.Resources >
....
            </Popup.Resources>
            <ContentControl Content ="{Binding Path=PopupRoot}" />
        </Popup>
    </Grid>

弹出窗口的IsOpen属性绑定到DataContext(类型为VM_Main)的IsRootPopupOpen属性。当单击Button并执行OpenPopup命令时,将此属性设置为true。在这里,命令以(静态)节点ID作为参数,并从模拟数据源加载根节点数据。因为根弹出窗口的StaysOpen设置为false,所以当用户单击弹出窗口外部或父窗口失焦时,整个弹出窗口“树”将关闭。

这是设置为主窗口DataSource的视图模型

    [ImplementPropertyChanged]
    public class VM_Main
    {
        public VM_Node PopupRoot { get; set; }
        public bool IsRootPopupOpen { get; set; }

        public VM_Main() { }

        private ICommand _OpenPopup;
        public ICommand OpenPopup
        {
            get { return _OpenPopup ?? 
            (_OpenPopup = new DelegateCommand(a => OpenPopupCommand(a))); }
        }
        private void OpenPopupCommand(object item)
        {
            if (item == null || !(item is int)) return;
            int nodeID = (int)item;
            this.PopupRoot = new VM_Node(DataAccessMockup.GetNodeData(nodeID));
            if (this.PopupRoot != null)
            {
               this.PopupRoot.LoadChildren();
               this.IsRootPopupOpen = true ;
            }
        }
    }

弹出窗口的布局由一个为VM_Node类型设置的DataTemplate在弹出窗口的Resources中管理。根弹出窗口中的ContentControlDataTemplate本身中的子弹出窗口中的ContentControl都将使用此DataTemplate(第二个“递归地”)

<Popup.Resources>
    <DataTemplate DataType="{x:Type VM:VM_Node}">
        <Border CornerRadius ="5" Background="AliceBlue" 
        BorderBrush="CornflowerBlue" BorderThickness="2" Padding ="3"
                        PreviewMouseDown="popup_Border_PreviewMouseDown">
            <StackPanel Orientation ="Vertical" >
                <TextBlock Text ="{Binding Path=NodeData.ID}" 
                Foreground="Black"/>
                <TextBlock Text ="{Binding Path=NodeData.Name}"  
                Foreground="Black"/>
                <Button Content ="My Button" Margin="5"/>
                <ListBox ItemsSource ="{Binding Path=ChildNodes.View, Mode=OneWay}"
                                 SelectionMode="Single"
                                 Width="Auto" Height ="Auto" 
                                 Padding="-1" Margin="0" 
                                 BorderThickness="0"
                                 IsSynchronizedWithCurrentItem="True"
                                 VirtualizingStackPanel.VirtualizationMode="Standard"
                                 VirtualizingStackPanel.IsVirtualizing="True"
                                 ScrollViewer.VerticalScrollBarVisibility="Disabled"
                                 ScrollViewer.HorizontalScrollBarVisibility="Disabled">
                    <ListBox.ItemsPanel>
                        <ItemsPanelTemplate>
                            <VirtualizingStackPanel Orientation ="Vertical" />
                        </ItemsPanelTemplate>
                    </ListBox.ItemsPanel>
                    <ListBox.ItemContainerStyle>
                        <Style TargetType="{x:Type ListBoxItem}">
                            <Style.Resources>
                                <SolidColorBrush x:Key="{x:Static SystemColors.ControlBrushKey}" 
                                Color="Transparent" />
                                <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" 
                                Color="Transparent"/>
                            </Style.Resources>
                            <Setter Property="HorizontalContentAlignment" 
                            Value="Stretch"/>
                            <Setter Property="VerticalContentAlignment" 
                            Value="Center"/>
                            <Setter Property="BorderBrush" 
                            Value="MediumBlue" />
                            <Setter Property="BorderThickness" Value="0" />
                            <Setter Property="Padding" Value="0" />
                            <Setter Property="Margin" Value="0" />
                        </Style>
                    </ListBox.ItemContainerStyle>
                    <ListBox.ItemTemplate>
                        <DataTemplate>
                            <Grid>
                                <ToggleButton Name ="btnClickMe"
                                           Height="20" Margin ="1" 
                                           Padding="3,1,3,1"
                                           Content="{Binding Path=NodeData.Name}"
                                           IsChecked="{Binding Path=IsSelected, 
                                           RelativeSource={RelativeSource 
                                             AncestorType=ListBoxItem}, Mode=TwoWay}"
                                           HorizontalAlignment="Stretch" 
                                           VerticalAlignment="Center"
                                           HorizontalContentAlignment="Center" 
                                           VerticalContentAlignment="Center" />
                                <Popup AllowsTransparency="True" StaysOpen="True"
                                               IsOpen="{Binding Path=IsSelected, 
                                               RelativeSource={RelativeSource 
                                                   AncestorType=ListBoxItem}, 
                                               Mode=OneWay}"
                                               PlacementTarget="{Binding ElementName=btnClickMe}"
                                               Placement="Bottom" PopupAnimation="Fade" >
                                    <ContentControl Content ="{Binding}" />
                                </Popup>
                            </Grid>
                        </DataTemplate>
                    </ListBox.ItemTemplate>
                </ListBox>
            </StackPanel>
        </Border>
    </DataTemplate >
</Popup.Resources>

PopupDataTemplate中的StaysOpen设置为true。这是因为在这里打开/关闭Popup完全由ListBox中的选择管理。或者更准确地说,由基础ItemsSource中的选择管理。

操作方法如下:

ListBox绑定到当前节点的子项

ItemsSource="{Binding Path=ChildNodes.View, Mode=OneWay}"

为了利用ViewableCollection类的功能(并避免内存泄漏),将集合的View绑定至关重要,而不是集合本身。

以确保ListBoxChildNodes.ViewCurrentItem同步

SelectionMode="Single"
IsSynchronizedWithCurrentItem="True"

ListBox中的项被选中时(这里是OneWay绑定),Popup打开/关闭

IsOpen="{Binding Path=IsSelected, 
RelativeSource={RelativeSource AncestorType=ListBoxItem}, Mode=OneWay}"

反过来,当用户勾选ToggleButton时,ListBoxItem被选中(这里是TwoWay绑定)

IsChecked="{Binding Path=IsSelected, 
RelativeSource={RelativeSource AncestorType=ListBoxItem}, Mode=TwoWay}"
>

ToggleButton被选中,ListBoxItem被选中,然后下一级Popup打开,ChildNodes.View中的CurrentItem改变,然后调用VM_Node中的这个事件处理程序,加载子节点

void ChildNodes_View_CurrentChanged(object sender, EventArgs e)
{
    if (this.ChildNodes.CurrentItem != null)
    {
         this.ChildNodes.CurrentItem.LoadChildren();
    }
}

现在,如果我们想让第4级和第3级的弹出窗口在单击第2级弹出窗口时关闭

  • 如果没有交互式内容,例如模板中的另一个Button,捕获MouseDown事件并清除选择就足够了
    private void popup_Border_MouseDown(object sender, MouseButtonEventArgs e)
    {
         ((VM_Node)((Border)sender).DataContext).ChildNodes.CurrentItem = null;
    }
  • 如果有交互式内容,例如模板中的另一个Button(如本例),我们将必须捕获PreviewMouseDown事件,因为按钮的Click事件会拦截MouseDown事件并处理它(弹出窗口不会关闭)
    private void popup_Border_PreviewMouseDown(object sender, MouseButtonEventArgs e)
    {
        var toggle = (ToggleButton)
          (e.OriginalSource as DependencyObject).TryFindParentBefore<ToggleButton , Popup>();
        if (toggle == null )
        {
             var parentPopupOrgSource = e.OriginalSource is Popup ? e.OriginalSource as Popup : 
              (e.OriginalSource as DependencyObject).TryFindParent<Popup>();
             var parentPopupSender = sender is Popup ? sender as Popup : 
              (sender as DependencyObject).TryFindParent<Popup>();
             if (parentPopupOrgSource != parentPopupSender) return;
             ((VM_Node)((Border)sender).DataContext).ChildNodes.CurrentItem = null;
        }
    }

如果在某个ToggleButton的空间内单击,则不执行任何操作并返回。然后,如果不是,由于PreviewMouseDown事件会冒泡并被调用多次,我们将必须确保我们处于正确的Popup中,即senderPopupOriginalSourcePopup相同。只有这样,我们才能清除选择。

关注点

这里使用的ViewableCollection类继承自ObservableCollection,并且是ListCollectionView的包装器。与本地实现的ObservableCollection相比,它为我们提供了一些优势。

  • 它可以批量加载项(使用ReplaceItems方法,该方法不会发送多个CollectionChanged通知)
  • 可以轻松使用ListCollectionViewCurrentItem属性;如果例如ListBox具有IsSynchronizedWithCurrentItem="True",则可以从视图模型进行选择管理;例如,用于清除ListBox上的选择
    MyViewableCollection.CurrentItem = null;
  • 它可以被序列化为XML,作为XmlArray(此处:未使用)
    [XmlArray]
    public ViewableCollection<MyType> MyViewableCollection { get; set; }
  • 从视图模型进行排序(此处:未使用)
    this.MyViewableCollection.View.SortDescriptions.Add
    (new SortDescription("MySortProperty", ListSortDirection.Ascending));
  • 从视图模型进行分组(此处:未使用)
    this.MyViewableCollection.View.GroupDescriptions.Add
    (new PropertyGroupDescription("MyGroupProperty"));
  • 从视图模型进行筛选(此处:未使用)
    this.MyViewableCollection.View.Filter = Name_FilterMethod;
    private bool Name_FilterMethod (object item)
    {
         VM_MyObject obj = item as VM_MyObject ;
         if (!obj.Name.Contains(this.FilterByName)) return true;
         return false;
    }
  • ListCollectionView实现了IEditableCollectionView,这使得修改(以及修改后刷新视图)集合/对象更加容易(此处:未使用)。它为何有用在此处得到了很好的解释 here
    myItem.SomeProperty=newValue;
    MyViewableCollection.EditableView.EditItem(myItem);
    MyViewableCollection.EditableView.CommitEdit();

这里显式使用ListCollectionView来管理集合/视图的想法来自 here,以防止在此处 here 描述的内存泄漏,通过显式地将单个ListCollectionView与单个ObservableCollection关联。

© . All rights reserved.