预选单选按钮 WPF MVVM

Preselect Radio Button WPF MVVM

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

页面之间的导航是这样完成的:

<Window.Resources>
        <DataTemplate DataType="{x:Type FirstViewModel}">
            <FirstView />
        </DataTemplate>
        <DataTemplate DataType="{x:Type SecondViewModel}">
            <SecondView />
        </DataTemplate>
</Window.Resources>

因此每次选择新页面时都会更新 DataContext。

遵循这种方法:

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

对 view/data 绑定没有影响。


您用来托管 StackPanelItemsControl 非常无用:它包含一个项目 - 包含所有按钮的 StackPanel。一般来说,避免 ItemsControl 并选择更高级的 ListBox。它具有一些重要的性能特征,例如 UI 虚拟化。

使用 ItemsControl 或更好的 ListBox 来托管 RadioButton 元素的集合是个好主意,但执行不佳。

您应该为导航按钮创建数据模型,当您添加更多页面并因此添加更多导航按钮时,这将特别方便。此模型上的 IsNavigating 属性 允许控制绑定到此 属性.

的按钮的状态

该模式与您用于页面的模式相同:View-Model-First。先创建数据模型,通过定义一个或多个DataTemplate让WPF动态渲染相关视图。在这种情况下,ListBox 将为您生成视图。
这就是 WPF 的主要概念:首先考虑数据。这就是 DataTemplate 的想法。

您的页面模型的 IPage.IsActive 属性 不应直接绑定到导航按钮。如果你真的需要这个 属性,那么在 MainViewModl 在旧页面模型 上重置这个 属性 之前 你替换 SelectedPage值(或您如何命名公开当前活动页面模型的 属性)并在将其分配给 SelectedPage 属性 后在新页面模型上设置此 属性。让托管和公开页面的模型处理和控制完整的导航逻辑。
虽然这个逻辑触发了视图,但是它是一个纯粹的模型相关逻辑。因此,您不应该拆分此逻辑并将其部分移动到视图(例如,通过数据绑定到视图,即使逻辑依赖于按钮)。
您甚至可以将此逻辑提取到新的 class,例如 PageModelController,然后 MainViewModel 可以使用和公开数据绑定。
考虑将 IPage.IsActive 属性 转换为只读 属性 并添加 IPage.Activate()IPage.Dactivate() 方法,如果更改 IsActive 状态涉及调用操作。

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 委托而不是专用工厂 class(为了可读性)。

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
然后在你的应用程序的入口点注册 INavigationItemFactory 工厂实现到你的 IoC 容器。

var services = new ServiceCollection();
services.AddSingleton<INavigationItemFactory, NavigationItemFactory>();