ComboBox WPF не отображает SelectedItem после одного DataTrigger, но делает для другого - PullRequest
5 голосов
/ 22 мая 2019

Итак, у меня есть выпадающий список, который я хотел бы использовать для нескольких наборов данных, а не иметь 3 отдельных выпадающих списка.Может быть, это плохо, и кто-то может сказать мне так.Я открыт для всех идей и предложений.Я просто пытаюсь очистить некоторый код и подумал, что один комбобокс, а не 3, был чище.В любом случае ItemsSource и SelectedItem all должны измениться при изменении другого значения ComboBox's, которое повышает значение свойства Changed для ComboBox, который не работает.Хуже всего то, что когда CurSetpoint.ActLowerModeIsTimerCondition истинно, он всегда правильно загружает SelectedItem, но при переходе от этого значения к CurSetpoint.ActLowerGseMode, являющемуся истинным, в поле со списком не загружается SelectedItem.

ВотXAML для ComboBox с проблемами.

<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, он ничего не загружает, несмотря на то, что его привязка не равна нулю и данные его экземпляра и экземпляра источника данных не изменились.Но если перейти от переменной к таймеру, таймер: 1 будет отображаться правильно.

enter image description here

Вот код модели, стоящий за изменяемым значением ComboBox режима.Наряду с двумя другими свойствами, которые являются SelectedItems для рассматриваемого ComboBox.Наряду со свойствами 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; 
   }
}

Я уверен, что, возможно, пропустил некоторую важную информацию, поэтому я буду обновлять ее любыми вопросами, которые у вас есть, или подробностями, которые вам нужны.

Обновление: просто хотел добавить, как указано в награду.Что если то, что я здесь делаю, не является хорошей идеей и есть лучший способ сделать это, кто-то с опытом, пожалуйста, просто скажите мне, почему и как я должен.Если есть лучшие способы, а то, что я делаю, плохо, мне просто нужно знать.

1 Ответ

5 голосов
/ 29 мая 2019

Нет ничего плохого в связывании нескольких ComboBox es и установке их Visibility.С одной стороны, это значительно снижает сложность по сравнению с кодом из вашего поста.

Тем не менее, вы можете легко поменять контекст (не путать с DataContext) на ItemsControl путем введения дополнительной абстракции между моделью представления и представлением.

Вот как это работает:

  1. Создание объекта context с соответствующими свойствами
  2. Примените контекст к вашему ItemsControl
  3. Позвольте де свойствам повторно привязать контекст изменилось

Ваша идея сборасвойства на единицу, безусловно, хорошие.Хотя реализация может быть лучше, и модель представления, и представление выглядят раздутыми.Вот для чего предназначен этот объект контекста, собирая и сохраняя состояние, когда вы меняете контексты назад и вперед.

Начиная с наших классов моделей.Давайте создадим код для интерфейса (даже если 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; }
    }
}

Далее, ViewModels, начиная с нашего контекста.

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>

Крутой особенностью Attached Property является то, что мы кодируем против абстрактного элемента управления Selector, который является прямым потомком ItemsControl.Таким образом, не изменяя наши нижние уровни, мы также можем делиться нашим контекстом с ListBox и DataGrid.

...