我有一个菜单,由单选按钮组成,用于在页面之间导航。当应用程序打开时,第一页被加载。
页面之间的导航是这样做的:
<Window.Resources>
<DataTemplate DataType="{x:Type FirstViewModel}">
<FirstView />
</DataTemplate>
<DataTemplate DataType="{x:Type SecondViewModel}">
<SecondView />
</DataTemplate>
</Window.Resources>
每次选择新页面时,DataContext都会更新。
遵循此方法:https://stackoverflow.com/a/61323201/17198402
MainView.xaml:
<Border Grid.Column="0">
<Grid Background="AliceBlue">
<Border
Width="10"
HorizontalAlignment="Left"
Background="SlateGray" />
<ItemsControl>
<StackPanel Orientation="Vertical">
<RadioButton
Command="{Binding ShowPageCommand}"
CommandParameter=//not important
IsChecked="{Binding IsActive, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource RadioButtonStyle}"
Content="First"/>
<RadioButton
Command="{Binding ShowPageCommand}"
CommandParameter=//not important
IsChecked="{Binding IsActive, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource RadioButtonStyle}"
Content="Second"/>
</StackPanel>
</ItemsControl>
</Grid>
</Border>
RadioButtonStyle:
<Style x:Key="RadioButtonStyle" TargetType="RadioButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type RadioButton}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Width="10">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="Transparent" />
<Style.Triggers> //MOST IMPORTANT!!
<DataTrigger Binding="{Binding Path=IsChecked, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ToggleButton}}}" Value="True">
<Setter Property="Background" Value="#D50005" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
<Border
Grid.Column="1"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
基本上,当一个单选按钮被点击时,它被绑定的页面被加载,按钮附近的边框变成红色——这就是它如何表明页面被打开。
IsActive
是每个页面的ViewModel
中的一个属性。
一切工作完美,然而,当我打开应用程序时,我希望第一个单选按钮已经被选中,并且它附近的边界是红色的。当我导航到另一个页面时,一切都按预期工作。
我试过了:
给第一个单选按钮一个名称,例如FirstRadioButton,并在
MainView.xaml.cs
中调用FirstRadioButton.IsChecked = true
。MainViewModel.cs
:
public MainViewModel(FirstViewModel firstViewModel, SecondViewModel secondViewModel)
{
firstViewModel.IsActive = true;
Pages = new Dictionary<PageName, IPage>
{
{ PageName.FirstView, firstViewModel },
{ PageName.SecondView, secondViewModel }
};
//other code..
}
public enum PageName
{
Undefined = 0,
FirstView = 1,
SecondView = 2
}
这个PageName
的东西是导航的一部分,我也注入ViewModels使用依赖注入。
正确的方法是什么?
从RadioButton.IsChecked
到IsActive
属性的数据绑定错误。它应该会在您的IDE中触发绑定错误。绑定试图找到一个不存在的MainViewModel.IsActive
属性。因此,设置
firstViewModel.IsActive = true
对视图/数据绑定没有影响。
您用来托管StackPanel
的ItemsControl
是相当无用的:它包含一个单独的项目-StackPanel
包含所有的按钮。一般来说,避免使用ItemsControl
,而选择更高级的ListBox
。它有一些重要的性能特性,比如UI虚拟化。
使用ItemsControl
或更好的ListBox
来托管RadioButton
元素的集合是一个好主意,但执行起来很糟糕。
您应该为导航按钮创建数据模型,当您添加更多页面并因此添加更多导航按钮时,这将特别方便。此模型上的IsNavigating
属性允许控制绑定到此属性的按钮的状态。
模式与您在页面中使用的模式相同:视图-模型-优先。首先创建数据模型,然后让WPF通过定义一个或多个DataTemplate
动态呈现相关视图。在本例中,ListBox
将为您生成视图。
这是WPF的主要概念:首先考虑数据。这就是DataTemplate
的理念所在。
页面模型的IPage.IsActive
属性不应该直接绑定到导航按钮。如果您真的需要这个属性,那么在MainViewModl
中重置旧页面模型上的这个属性,然后在之前替换SelectedPage
值(或者您如何命名暴露当前活动页面模型的属性),并在将其分配给SelectedPage
属性后在新页面模型上设置这个属性。让承载和公开页面的模型处理和控制完整的导航逻辑。
虽然这个逻辑触发了视图,但它是一个纯粹的与模型相关的逻辑。因此,你不应该拆分这个逻辑并将其部分移动到视图中(例如,通过数据绑定到视图中)。使逻辑依赖于按钮)。
你甚至可以将这个逻辑提取到一个新的类中,例如PageModelController
,然后MainViewModel
可以使用并公开数据绑定。
如果更改IsActive
状态涉及调用操作,请考虑将IPage.IsActive
属性转换为只读属性并添加IPage.Activate()
和IPage.Dactivate()
方法。
NavigationItem.cs
导航按钮数据模型。
class NavigationItem : INotifyPropertyChanged
{
public NavigationItem(string name, PageName pageId, bool isNavigating = false)
{
this.Name = name;
this.IsNavigating = isNavigating;
this.PageId = pageId;
}
public string Name { get; }
public PageName PageId { get; }
private bool isNavigating
public bool IsNavigating
{
get => this.isNavigating;
set
{
this.isNavigating = value;
OnPropertyChanged();
}
}
public PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyname = "")
=> this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
INavigationItemFactory.cs
既然使用了依赖注入,就应该定义一个抽象工厂来动态创建实例。如果创建一个NavigationItem
需要少于三个参数,我会选择一个Func
委托而不是一个专用的工厂类(为了可读性)。
interface INavigationItemFactory
{
NavigationItem Create(string name, PageName pageId, bool isNavigating = false);
}
NavigationItemFactory.cs
class NavigationItemFactory
{
public NavigationItem Create(string name, PageName pageId, bool isNavigating = false)
=> new NavigationItem(name, pageId, isNavigating);
}
MainViewModel.cs
创建单选按钮的数据模型。
class MainViewModel : INotifyPropertyChanged
{
public ObservableCollection<NavigationItem> NavigationItems { get; }
private INavigationItemFactory NavigationItemFactory { get; }
public MainViewModel(INavigationItemFactory navigationItemFactory)
{
this.NavigationItemFactory = navigationItemFactory;
this.NavigationItems = new ObservableCollection<NavigationItem>
{
this.NavigationItemFactory.Create("First Page", PageName.FirstView, true), // Preselect the related RadioButton
this.NavigationItemFactory.Create("Second Page", PageName.SecondView),
this.NavigationItemFactory.Create("Third Page", PageName.ThirdView)
};
}
// Handle page selection and the IsActive state of the pages.
// Consider to make the IsActive property read-only and add Activate() and Dactivate() methods,
// if changing this state involvs invoking operations.
public void SelectPage(object param)
{
if (param is PageName pageName
&& this.Pages.TryGetValue(pageName, out IPage selectedPage))
{
// Deactivate the old page
this.SelectedPage.IsActive = false;
this.SelectedPage = selectedPage;
// Activate the new page
this.SelectedPage.IsActive = true;
}
}
}
MainView.xaml
示例期望MainViewModel
是MainView
的DataContext
。
<Window>
<!-- Navigation bar (vertical - to change it to horizontal change the ListBox.ItemPanel) -->
<ListBox ItemsSource="{Binding NavigationItems}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type local:NavigationItem}">
<RadioButton GroupName="PageNavigationButtons"
Content="{Binding Name}"
IsChecked="{Binding IsNavigating}"
CommandParameter="{Binding PageId}"
Command="{Binding RelativeSource={RelativeSource AncestorType=ListBox}, Path=DataContext.ShowPageCommand}" />
</DataTemplate>
</ListBox.ItemTemplate>
<!-- Remove the ListBox look&feel by overriding the ControlTemplate of ListBoxItem -->
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<ContentPresenter />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Window>
您还可以简化您的RadioButtonStyle
.
一般来说,当您的触发器目标元素是ControlTemplate
的一部分时,最好对每个单独的元素使用通用的ControlTemplate.Triggers
而不是Style.Triggers
。将所有触发器放在一个地方,而不是分散在整个模板中,只在布局中添加杂音,这样也更简洁:
<Style x:Key="RadioButtonStyle" TargetType="RadioButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type RadioButton}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border x:Name="IsActiveIndicator"
Grid.Column="0"
Background="Transparent"
Width="10" />
<Border Grid.Column="1"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="IsActiveIndicator" Property="Background" Value="#D50005" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
App.xaml.cs
然后在应用程序的入口点用IoC容器注册INavigationItemFactory
工厂实现。
var services = new ServiceCollection();
services.AddSingleton<INavigationItemFactory, NavigationItemFactory>();