预选单选按钮WPF MVVM



我有一个菜单,由单选按钮组成,用于在页面之间导航。当应用程序打开时,第一页被加载。

页面之间的导航是这样做的:

<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中的一个属性。

一切工作完美,然而,当我打开应用程序时,我希望第一个单选按钮已经被选中,并且它附近的边界是红色的。当我导航到另一个页面时,一切都按预期工作。

我试过了:

  1. 给第一个单选按钮一个名称,例如FirstRadioButton,并在MainView.xaml.cs中调用FirstRadioButton.IsChecked = true

  2. 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.IsCheckedIsActive属性的数据绑定错误。它应该会在您的IDE中触发绑定错误。绑定试图找到一个不存在的MainViewModel.IsActive属性。因此,设置

firstViewModel.IsActive = true

对视图/数据绑定没有影响。


您用来托管StackPanelItemsControl是相当无用的:它包含一个单独的项目-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
示例期望MainViewModelMainViewDataContext

<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>();

最新更新