WPF 多级弹出窗口,绑定到分层树
本文介绍了使用弹出窗口显示分层树所需的最小代码。
引言
本文介绍了使用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
中的选择:当ChildNodes
的CurrentItem
发生变化时,如果需要,将加载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
中管理。根弹出窗口中的ContentControl
和DataTemplate
本身中的子弹出窗口中的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>
Popup
在DataTemplate
中的StaysOpen
设置为true
。这是因为在这里打开/关闭Popup
完全由ListBox
中的选择管理。或者更准确地说,由基础ItemsSource
中的选择管理。
操作方法如下:
ListBox
绑定到当前节点的子项
ItemsSource="{Binding Path=ChildNodes.View, Mode=OneWay}"
为了利用ViewableCollection
类的功能(并避免内存泄漏),将集合的View
绑定至关重要,而不是集合本身。
以确保ListBox
与ChildNodes.View
的CurrentItem
同步
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
中,即sender
的Popup
与OriginalSource
的Popup
相同。只有这样,我们才能清除选择。
关注点
这里使用的ViewableCollection
类继承自ObservableCollection
,并且是ListCollectionView
的包装器。与本地实现的ObservableCollection
相比,它为我们提供了一些优势。
- 它可以批量加载项(使用
ReplaceItems
方法,该方法不会发送多个CollectionChanged
通知) - 可以轻松使用
ListCollectionView
的CurrentItem
属性;如果例如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
关联。