【问题标题】:WPF ComboBox Doesn't Display SelectedItem after one DataTrigger but does for anotherWPF ComboBox 在一个 DataTrigger 之后不显示 SelectedItem 但在另一个 DataTrigger 之后显示
【发布时间】:2019-05-22 20:08:29
【问题描述】:

所以我有一个组合框,我想为多组数据重复使用,而不是拥有 3 个单独的组合框。也许这很糟糕,有人可以告诉我。我对所有想法和建议持开放态度。我只是想清理一些代码,并认为一个组合框而不是 3 更干净。无论如何,ItemsSourceSelectedItem 都应该在另一个 ComboBox'svalue 发生变化时发生变化,这会提高不起作用的 ComboBox 的 Property Changed 值。最糟糕的部分是当CurSetpoint.ActLowerModeIsTimerCondition为真时,它总是正确加载SelectedItem,但是当从那个到CurSetpoint.ActLowerGseMode为真时,组合框没有加载SelectedItem

这是有问题的 ComboBox 的 XAML。

<ComboBox Grid.Row="1" Grid.Column="1" Margin="5,2" VerticalAlignment="Center" Name="cmbActTimersSetpointsGseVars">
       <ComboBox.Style>
          <Style BasedOn="{StaticResource {x:Type ComboBox}}" TargetType="{x:Type ComboBox}">
             <Style.Triggers>
                <DataTrigger Binding="{Binding Path=CurSetpoint.ActLowerModeIsTimerCondition}" Value="True">
                   <Setter Property="ItemsSource" Value="{Binding TimerInstances}" />
                   <Setter Property="SelectedItem" Value="{Binding CurSetpoint.ActLowerTimerInstance, Mode=TwoWay}" />
                   <Setter Property="DisplayMemberPath" Value="DisplayName"></Setter>
                   <Setter Property="Visibility" Value="Visible" />
                </DataTrigger>
                <DataTrigger Binding="{Binding Path=CurSetpoint.ActLowerGseMode}" Value="True">
                   <Setter Property="ItemsSource" Value="{Binding EnabledGseVars}" />
                   <Setter Property="SelectedItem" Value="{Binding CurSetpoint.ActLowerGseVar, Mode=TwoWay}" />
                   <Setter Property="DisplayMemberPath" Value="DisplayName"></Setter>
                   <Setter Property="Visibility" Value="Visible" />
                </DataTrigger>
                <DataTrigger Binding="{Binding Path=CurSetpoint.ActModeIsLogicCondition}" Value="True">
                   <Setter Property="ItemsSource" Value="{Binding SetpointStates}" />
                   <Setter Property="SelectedItem" Value="{Binding CurSetpoint.ActSetpoint1State, Mode=TwoWay}" />
                   <Setter Property="DisplayMemberPath" Value="Value"></Setter>
                   <Setter Property="Visibility" Value="Visible" />
                </DataTrigger>
                <DataTrigger Binding="{Binding Path=CurSetpoint.ShowActLowerCmbBox}" Value="False">
                   <Setter Property="Visibility" Value="Collapsed" />
                </DataTrigger>
             </Style.Triggers>
          </Style>
       </ComboBox.Style>
</ComboBox>

这是两个组合框的图像。当模式从 Timer 更改为 Variable 时,它​​不会加载任何内容,尽管它的绑定不是 null 并且它的实例和 itemssource 实例数据没有改变。但是如果我从变量转到计时器,计时器:1 会正确显示。

这是模式组合框值被更改后的模型代码。连同其他两个属性,即相关 ComboBox 的 SelectedItems。以及ItemsSource的属性

private VarItem actLowerMode;
public VarItem ActLowerMode
{
   get { return this.actLowerMode; }
   set
   {
      if (value != null)
      {
         var oldValue = this.actLowerMode;

         this.actLowerMode = value;
         config.ActLowerMode.Value = value.ID;

         //if they weren't the same we need to reset the variable name
         //Note: 5/21/19 Needs to be this way instead of before because when changing from Timer->GseVariable it wouldn't change the config value because it
         //thought it was still a timer condition because the value hadn't been changed yet.
         if (oldValue != null && (oldValue.CheckAttribute("timer") != value.CheckAttribute("timer")))
         {
            if (value.CheckAttribute("timer"))
            {
               ActLowerTimerInstance = model.TimerInstances.First();
            }
            else
            {
               ActLowerVarName = "";
               if (GseMode)
               {
                  ActLowerGseVar = model.EnabledGseVars.FirstOrDefault();
               }
            }
         }

         RaisePropertyChanged("ActLowerMode");
         RaisePropertyChanged("HasActLowerScale");
         RaisePropertyChanged("ActLowerGseMode");
         RaisePropertyChanged("HasActLowerVarName");
         RaisePropertyChanged("ActLowerModeIsConstant");
         RaisePropertyChanged("ActLowerRow1Label");
         RaisePropertyChanged("ActLowerModeIsTimerCondition");
         RaisePropertyChanged("ShowActLowerConstTextBox");
         RaisePropertyChanged("ShowActLowerCmbBox");
         RaisePropertyChanged("ShowActLowerRow1Label");
         if (GseMode)
         {
            RaisePropertyChanged("ActLowerGseMode");
         }
      }
   }
}

private GseVariableModel actLowerGseVar;
public GseVariableModel ActLowerGseVar
{
   get { return this.actLowerGseVar; }
   set
   {
      if (value != null)
      {
         this.actLowerGseVar = value;
         if (!ActLowerModeIsTimerCondition)//only changing the config value if its not set to timer
         {
            config.ActLowerVarName.Value = value.Number.ToString();
         }
         RaisePropertyChanged("ActLowerGseVar");
      }
   }
}      

private INumberedSelection actLowerTimerInstance;
public INumberedSelection ActLowerTimerInstance
{
   get { return this.actLowerTimerInstance; }
   set
   {
      if (value != null)
      {
         this.actLowerTimerInstance = value;
         config.ActLowerVarName.Value = value.Number.ToString();
         RaisePropertyChanged("ActLowerTimerInstance");
      }
   }
}

public ObservableCollection<INumberedSelection> TimerInstances { get { return this.timerInstances; } }

public ObservableCollection<GseVariableModel> EnabledGseVars
{
   get 
   {
      return enabledGseVariables; 
   }
}

我确定我可能忽略了一些重要信息,因此我会根据你们的任何问题或需要的详细信息进行更新。

更新:只是想按照赏金中的说明添加。如果我在这里做的不是一个好主意,并且有更好的方法,请有经验的人告诉我为什么以及如何做。如果有更好的方法,而我做的不好,我只需要知道。

【问题讨论】:

  • 欢迎加入WPF Chat Room讨论...
  • 是否可以将 CurSetpoint 切换为使用枚举而不是 3 个布尔值?我很好奇只有​​ true 的条件检查的 3 个 DataTriggers 是否可能是一个问题。 IE。当ActModeIsLogicConditionActLowerModeIsTimerCondition 都为真时会发生什么?还是那不可能?
  • @GingerNinja 应该不可能,但会仔细检查逻辑并回复您。通过将其切换为枚举,您的说法就像拥有 CurSetpoint.ActLowerEnumValue 之类的东西并让它寻找说 1、2、3 或 4 类型的场景?
  • @Birdbuster 是的。因此,您的 DataTrigger 将像 switch 语句一样工作,而不是连续的 if 语句。通常,如果我在一组触发器中有两个以上的选项,我会尝试使用枚举。

标签: c# wpf selecteditem datatrigger itemssource


【解决方案1】:

绑定多个ComboBoxes并设置它们的Visibility并没有错。一方面,与您帖子中的代码相比,它大大降低了复杂性。

尽管如此,您可以通过在视图模型和视图之间引入额外的抽象来轻松交换ItemsControl上下文(不要与DataContext 混淆)。

这就是它的工作原理:

  1. 创建具有相关属性的上下文对象
  2. 上下文应用到您的ItemsControl
  3. 让 de 属性在 context 更改时重新绑定

您收集每个实体的属性的想法当然是一个好主意。尽管实现可能会更好,但视图模型和视图看起来都很臃肿。这就是上下文对象的全部意义,在您来回交换上下文时收集和保持状态。

从我们的模型类开始。让我们针对接口编写代码(即使 ItemsSource 没有类型)。

namespace WpfApp.Models
{
    public interface IEntity
    {
        string Name { get; }
    }

    public class Dog : IEntity
    {
        public Dog(string breed, string name)
        {
            Breed = breed;
            Name = name;
        }

        public string Breed { get; }
        public string Name { get; }
    }

    public class Author : IEntity
    {
        public Author(string genre, string name)
        {
            Genre = genre;
            Name = name;
        }

        public string Genre { get; }
        public string Name { get; }
    }
}

接下来是 ViewModel,从我们的上下文开始。

namespace WpfApp.ViewModels
{
    public class ItemsContext : ViewModelBase
    {
        public ItemsContext(IEnumerable<IEntity> items)
        {
            if (items == null || !items.Any()) throw new ArgumentException(nameof(Items));

            Items = new ObservableCollection<IEntity>(items);
            SelectedItem = Items.First();
        }

        public ObservableCollection<IEntity> Items { get; }

        private IEntity selectedItem;
        public IEntity SelectedItem
        {
            get { return selectedItem; }
            set
            {
                selectedItem = value;
                OnPropertyChanged();
            }
        }

        public string DisplayMemberPath { get; set; }
    }
}

如前所述,带有SelectedItem 通知的相关属性并没有什么特别之处。我们立即看到对我们的MainViewModel 的影响。

namespace WpfApp.ViewModels
{
    public class MainViewModel : ViewModelBase
    {
        private readonly ItemsContext _dogContext;
        private readonly ItemsContext _authorContext;

        public MainViewModel()
        {
            _dogContext = new ItemsContext(FetchDogs()) { DisplayMemberPath = nameof(Dog.Breed) };
            _authorContext = new ItemsContext(FetchAuthors()) { DisplayMemberPath = nameof(Author.Genre) };
        }

        private ItemsContext selectedContext;
        public ItemsContext SelectedContext
        {
            get { return selectedContext; }
            set
            {
                selectedContext = value;
                OnPropertyChanged();
            }
        }

        private bool dogChecked;
        public bool DogChecked
        {
            get { return dogChecked; }
            set
            {
                dogChecked = value;
                if(dogChecked) SelectedContext = _dogContext;
            }
        }

        private bool authorChecked;
        public bool AuthorChecked
        {
            get { return authorChecked; }
            set
            {
                authorChecked = value;
                if(authorChecked) SelectedContext = _authorContext;
            }
        }

        private static IEnumerable<IEntity> FetchDogs() =>
            new List<IEntity>
            {
                new Dog("Terrier", "Ralph"),
                new Dog("Beagle", "Eddy"),
                new Dog("Poodle", "Fifi")
            };

        private static IEnumerable<IEntity> FetchAuthors() =>
            new List<IEntity>
            {
                new Author("SciFi", "Bradbury"),
                new Author("RomCom", "James")
            };
    }
}

两个完全分离的流,每个流管理自己的上下文。很明显,您可以轻松地将其扩展到任意数量的上下文,而不会相互干扰。现在,要将上下文应用到我们的ItemsControl,我们有两个选项。我们可以继承 Control 或使用附加属性。优先考虑组合而不是继承,这是带有 AP 的类。

namespace WpfApp.Extensions
{
    public class Selector
    {
        public static ItemsContext GetContext(DependencyObject obj) => (ItemsContext)obj.GetValue(ContextProperty);
        public static void SetContext(DependencyObject obj, ItemsContext value) => obj.SetValue(ContextProperty, value);

        public static readonly DependencyProperty ContextProperty =
            DependencyProperty.RegisterAttached("Context", typeof(ItemsContext), typeof(Selector), new PropertyMetadata(null, OnItemsContextChanged));

        private static void OnItemsContextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var selector = (System.Windows.Controls.Primitives.Selector)d;
            var ctx = (ItemsContext)e.NewValue;

            if (e.OldValue != null) // Clean up bindings from previous context, if any
            {
                BindingOperations.ClearBinding(selector, System.Windows.Controls.Primitives.Selector.SelectedItemProperty);
                BindingOperations.ClearBinding(selector, ItemsControl.ItemsSourceProperty);
                BindingOperations.ClearBinding(selector, ItemsControl.DisplayMemberPathProperty);
            }

            selector.SetBinding(System.Windows.Controls.Primitives.Selector.SelectedItemProperty, new Binding(nameof(ItemsContext.SelectedItem)) { Source = ctx, Mode = BindingMode.TwoWay });
            selector.SetBinding(ItemsControl.ItemsSourceProperty, new Binding(nameof(ItemsContext.Items)) { Source = ctx });
            selector.SetBinding(ItemsControl.DisplayMemberPathProperty, new Binding(nameof(ItemsContext.DisplayMemberPath)) { Source = ctx });
        }
    }
}

这涵盖了第 2 步和第 3 步。您可以随意调整它。例如,我们将ItemsContext.DisplayMemberPath 设置为非通知属性,因此您可以直接设置值而不是通过绑定。

最后是视图,所有这些都汇集在一起​​。

<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:vm="clr-namespace:WpfApp.ViewModels"
        xmlns:ext="clr-namespace:WpfApp.Extensions"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800" WindowStartupLocation="CenterScreen">
    <Window.DataContext>
        <vm:MainViewModel/>
    </Window.DataContext>
    <Window.Resources>
        <Style x:Key="SelectorStyle" TargetType="{x:Type Selector}">
            <Setter Property="Width" Value="150"/>
            <Setter Property="HorizontalAlignment" Value="Left"/>
            <Setter Property="Margin" Value="0,20"/>
        </Style>
    </Window.Resources>
    <StackPanel Margin="20">
        <RadioButton GroupName="Entities" Content="Dogs" IsChecked="{Binding DogChecked}" />
        <RadioButton GroupName="Entities" Content="Authors" IsChecked="{Binding AuthorChecked}" />
        <ComboBox ext:Selector.Context="{Binding SelectedContext}" Style="{StaticResource SelectorStyle}" />
        <ListBox  ext:Selector.Context="{Binding SelectedContext}" Style="{StaticResource SelectorStyle}" />
        <DataGrid ext:Selector.Context="{Binding SelectedContext}" Style="{StaticResource SelectorStyle}" />
    </StackPanel>
</Window>

附加属性很酷的一点是,我们针对抽象的Selector 控件进行编码,它是ItemsControl 的直接后代。因此,无需更改我们的较低层,我们也可以与 ListBoxDataGrid 共享我们的上下文。

【讨论】:

  • 我喜欢这里的发展方向。因为我实际上已经在我可以使用的许多这些属性之间拥有了一个共享接口。虽然我想复制粘贴你在这里得到的东西,只是为了看看它是否正常工作,但我遇到了一些小问题。ViewModelBase 是什么?其次不是一件大事,但所有属性都缺少集合?我认为? FetchDogsFetchAuthors 似乎也对某些语法不满意。 (从来没有那样做,但我想看看它如何使用正确的语法)。最后nameof 似乎是未知数。我目前正在使用 VS2010 和 .Net 4.0
  • @Birdbuster FetchDogsFetchAuthors 使用表达式主体定义来定义这些方法,docs.microsoft.com/en-us/dotnet/csharp/language-reference/…。您可以更改定义以使用 return List&lt;IEntity&gt; 而不是 =&gt; 运算符。 nameof 运算符是在 c# 6.0 中添加的,但我不确定是否有办法让它与 .Net 4.0 一起使用。如果您不能使用更高版本的.Net,也许这个答案是一个解决方案。 stackoverflow.com/a/31262225/10858684。我猜ViewModelBase 只是定义了OnPropertyChanged
  • @Birdbuster 很高兴你喜欢它!设置器并没有“丢失”,我选择使用immutable objects 作为模型类。只是我的一个偏好。我已将解决方案移植到 .NET4,您可以在 GitHub 上找到它。我不再使用 VS2010,但如果遇到迁移问题,您应该能够复制文件。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-04-30
  • 2014-04-08
  • 1970-01-01
  • 1970-01-01
  • 2022-06-20
  • 1970-01-01
相关资源
最近更新 更多