WPF уведомил поле выбора ComboBox и элемент раскрывающегося списка не синхронизируются и не обновляются быстро

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

В форме примера есть 2 поля со списком, показывающих предварительно загруженных людей, а также кнопку для добавления человека и кнопку для изменения некоторых данных о существующем человеке. При щелчке по нему поле Person.Type изменяется немного случайным образом, и другая строка файла изображения сохраняется в Person.PicFullPath.

<Window x:Class="combobox_test__01.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:combobox_test__01"
        mc:Ignorable="d"
        Title="MainWindow" Height="600" Width="420" WindowStartupLocation="CenterScreen">
    <Window.Resources>
        <local:VM x:Key="vm" />

        <DataTemplate x:Key="cboTemplate" >
            <StackPanel Orientation="Horizontal">
                <Border Height="80" Width="80" >
                    <Image>
                        <Image.Source>
                            <PriorityBinding>
                                <Binding Path="PicFullPath"  Mode="TwoWay"/>
                            </PriorityBinding>
                        </Image.Source>
                    </Image>
                </Border>
                <Canvas Height="80" Width="160" >
                    <StackPanel>
                        <TextBlock HorizontalAlignment="Left" Height="16" Text="{Binding Path=Name, Mode=TwoWay}"  />
                        <TextBlock HorizontalAlignment="Left" Height="16" Text="{Binding Path=Type, Mode=TwoWay}"  />
                    </StackPanel>
                </Canvas>
            </StackPanel>
        </DataTemplate>

    </Window.Resources>
    <Window.DataContext>
        <Binding Source="{StaticResource vm}" />
    </Window.DataContext>
    <Grid>
        <ComboBox Name="cbo1" IsSynchronizedWithCurrentItem="True" HorizontalAlignment="Left" Margin="19,11,0,0" VerticalAlignment="Top" Width="159" 
                ItemsSource="{Binding People}" 
                SelectedItem="{Binding SelectedPerson}"  
                DisplayMemberPath="Type" />
        <Button Content="New Row" HorizontalAlignment="Left" Margin="224,11,0,0" VerticalAlignment="Top" Width="142" 
                Command="{Binding NewRowCommand}" />
        <Button Content="Change current row data" HorizontalAlignment="Left" Margin="224,48,0,0" VerticalAlignment="Top" Width="142" 
                Command="{Binding AlterRowCommand}" />
        <ComboBox Name="cbo2" IsSynchronizedWithCurrentItem="True" HorizontalAlignment="Left" Margin="19,130,0,0" VerticalAlignment="Top" Width="159" Height="82" 
            ItemsSource="{Binding People}"
            SelectedItem="{Binding SelectedPerson}" 
            ItemTemplate="{StaticResource cboTemplate}" />
    </Grid>
</Window>

using myAssemblies;
using System;
using System.Collections.ObjectModel;
using System.Windows.Input;
using System.ComponentModel;

namespace combobox_test__01
{
    public class VM : INotifyPropertyChanged
    {
        private ObservableCollection<Person> people;
        public ObservableCollection<Person> People
        {
            get { return people; }
            set
            {
                 people = value;
                OnPropertyChanged("People");
            }
        }

        private Person selectedPerson;
        public Person SelectedPerson
        {
            get { return selectedPerson; }
            set
            {
                selectedPerson = value;
                OnPropertyChanged("SelectedPerson");
                People = People;
            }
        }

        private int idx = 0;
        private string[,] newP;
        private string DefaultPic = @"D:\Visual Studio Testing\Icons\user-icon.png",
            NewPic = @"D:\Visual Studio Testing\Small size pictures\mugshot";
        private Random random = new Random();

        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this,
                    new PropertyChangedEventArgs(propertyName));
            }
        }

        public VM()
        {
            People = new ObservableCollection<Person>()
            {
                new Person() {Name="Aa", Type="Taa", PicFullPath=DefaultPic},
                new Person() {Name="Bb", Type="Tbb", PicFullPath=DefaultPic},
                new Person() {Name="Cc", Type="Tcc", PicFullPath=DefaultPic}
            };

            newP = new string[4, 3] { { "Dd", "Tdd", "" }, { "Ee", "Tee", "" }, { "Ff", "Tff", "" }, { "Gg", "Tgg", "" } };
        }

        public ICommand AlterRowCommand => new RelayCommandBase(AlterRow);
        private void AlterRow(object parameter)
        {
            //Make SelectedPerson.Type into a string ending with 2 randomish digits
            string fmts, picChoice;
            GetDifferentData(out fmts, out picChoice);
            string s = SelectedPerson.Type.Substring(0,3).PadRight(5);
            SelectedPerson.Type = s + fmts;

            // Use those 2 randomish digits to choose a picture from existing ones
            SelectedPerson.PicFullPath = NewPic + picChoice;

            // Force both setters to execute
            SelectedPerson = SelectedPerson;
        }

        public ICommand NewRowCommand => new RelayCommandBase(NewRow);
        private void NewRow(object parameter)
        {
            string fmts, picChoice;
            GetDifferentData(out fmts, out picChoice);
            People.Add(new Person() { Name = newP[idx, 0], Type = newP[idx++, 1], PicFullPath = NewPic + picChoice });
        }

        private void GetDifferentData(out string fmts, out string picChoice)
        {
            int randomi = random.Next(1, 35);
            fmts = randomi.ToString("D2");
            picChoice = fmts + ".jpg";
        }
    }

    public class Person
    {
        public string Name { get; set; }
        public string Type { get; set; }
        public string PicFullPath { get; set; }
    }
}

Это поведение:

Запустите приложение
Модель представления находится в Состоянии 1
Нажмите «Изменить данные текущей строки»
Модель представления находится в Состоянии 2
без видимых изменений: Я хочу, чтобы в полях выбора отображалось измененное Type данные и другая картина

Щелкните поле со списком cbo1, и в раскрывающемся списке отобразится изменение People.Type. В поле выбора по-прежнему отображаются старые данные.

Щелкните поле со списком cbo2, раскрывающийся список показывает изменение People.Type и другое изображение. В поле выбора по-прежнему отображаются старые данные.

Снова нажмите «Изменить данные текущей строки», не перемещая выбранный элемент.

Модель представления находится в состоянии 3.
никаких видимых изменений: при убранном раскрывающемся списке форма выглядит так же, как при инициализации.

Щелкните поле со списком cbo1. В раскрывающемся списке по-прежнему отображается первое изменение People.Type, но People.Type было изменено снова. В поле выбора по-прежнему отображаются исходные данные.

Щелкните поле со списком cbo2, раскрывающийся список не изменился с момента последнего выпадения. В поле выбора по-прежнему отображаются исходные данные.

В модели представления было 3 изменения состояния, но поля со списком показывают Состояние 1 в поле выбора и Состояние 2 в раскрывающемся списке. Я хочу, чтобы они показали Состояние 3 в обоих местах.

Щелкните поле со списком cbo1 и выберите другой элемент, затем выберите первый элемент. Итак, мы переместили выбранный элемент и переместили его обратно.
оба поля со списком показывают последние данные в поле выбора
раскрывающиеся списки устарели. Оба показывают Состояние 2, поэтому изменение выбора и его возврат не изменили отображаемые данные в раскрывающемся списке.

Нажмите «Новая строка».
Модель представления находится в Состоянии 4
без видимых изменений: как и ожидалось - новый человек не выбран, поэтому видимых изменений не будет.

Щелкните поля со списком
Новый человек отображается в раскрывающемся списке, но первый элемент по-прежнему показывает Состояние 2.

При отсутствии щелчка в раскрывающихся списках отображаются изменения после первого изменения - они застревают, показывая Состояние 2.

Как сделать так, чтобы в полях выбора и раскрывающихся списках всегда отображались самые свежие данные?


person Moonling    schedule 18.11.2017    source источник


Ответы (1)


У меня есть решение. Это не очень чисто, но просто и работает. Это обновленный код модели представления с небольшими изменениями и новым методом:

using npAssemblies;
using System;
using System.Collections.ObjectModel;
using System.Windows.Input;
using System.ComponentModel;

namespace combobox_test__01
{
    public class VM : INotifyPropertyChanged
    {
        private ObservableCollection<Person> people;
        public ObservableCollection<Person> People
        {
            get { return people; }
            set
            {
                 people = value;
                OnPropertyChanged("People");
            }
        }

        private Person selectedPerson;
        public Person SelectedPerson
        {
            get { return selectedPerson; }
            set
            {
                selectedPerson = value;
                OnPropertyChanged("SelectedPerson");
            }
        }

        private int cboPeopleSelectedIndex;
        public int CboPeopleSelectedIndex
        {
            get { return cboPeopleSelectedIndex; }
            set
            {
                cboPeopleSelectedIndex = value;
                OnPropertyChanged("CboPeopleSelectedIndex");
            }
        }

        private int newPidx = 0;
        private string[,] newP;
        private string DefaultPic = @"D:\Visual Studio Testing\Icons\user-icon.png",
            NewPic = @"D:\Visual Studio Testing\Small size pictures\mugshot";
        private Random random = new Random();

        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this,
                    new PropertyChangedEventArgs(propertyName));
            }
        }

        public VM()
        {
            People = new ObservableCollection<Person>()
            {
                new Person() {Name="Aa", Type="Taa", PicFullPath=DefaultPic},
                new Person() {Name="Bb", Type="Tbb", PicFullPath=DefaultPic},
                new Person() {Name="Cc", Type="Tcc", PicFullPath=DefaultPic}
            };

            newP = new string[4, 3] { { "Dd", "Tdd", "" }, { "Ee", "Tee", "" }, { "Ff", "Tff", "" }, { "Gg", "Tgg", "" } };
        }

        public ICommand AlterRowCommand => new RelayCommandBase(AlterRow);
        private void AlterRow(object parameter)
        {
            //Make SelectedPerson.Type into a string ending with 2 randomish digits
            string fmts, picChoice;
            GetDifferentData(out fmts, out picChoice);
            string s = SelectedPerson.Type.Substring(0,3).PadRight(5);
            SelectedPerson.Type = s + fmts;
            // Use those 2 randomish digits to choose a picture from existing ones
            SelectedPerson.PicFullPath = NewPic + picChoice;
            // refresh the control the collection is bound to
            ResetBoundItems(SelectedPerson);
        }

        public ICommand NewRowCommand => new RelayCommandBase(NewRow);
        private void NewRow(object parameter)
        {
            string fmts, picChoice;
            int highIdx = People.Count;
            GetDifferentData(out fmts, out picChoice);
            Person newPerson = new Person() { Name = newP[newPidx, 0], Type = newP[newPidx++, 1], PicFullPath = NewPic + picChoice };
            People.Add(newPerson);
            // refresh the control the collection is bound to and select the new row
            ResetBoundItems(newPerson, highIdx);
        }

        private void GetDifferentData(out string fmts, out string picChoice)
        {
            int randomi = random.Next(1, 35);
            fmts = randomi.ToString("D2");
            picChoice = fmts + ".jpg";
        }

        private void ResetBoundItems(Person inPerson, int gotoIndex = -1)
        {
            // refreshes the display of the combobox otherwise it is visually stale
            int idx = gotoIndex;
            if (gotoIndex == -1)
            {
                // a preferred index was not supplied; use the currently selected index
                idx = CboPeopleSelectedIndex;
            }
            // save a copy of the current selected person
            Person tmpP = (Person)inPerson.Clone();
            // save the current ItemsSource
            ObservableCollection<Person> refPC = new ObservableCollection<Person>();
            refPC = People;
            // reset the ItemsSource
            ObservableCollection<Person> tmpPC = new ObservableCollection<Person>();
            People = tmpPC;
            // set it back
            People = refPC;
            // restore the selected person
            SelectedPerson = (Person)tmpP.Clone();
            // select the relevant row
            CboPeopleSelectedIndex = idx;
        }
    }

    public class Person
    {
        public string Name { get; set; }
        public string Type { get; set; }
        public string PicFullPath { get; set; }

        public Person Clone()
        {
            return (Person)this.MemberwiseClone();
        }
    }
}

Представлению необходимо, чтобы у ComboBoxes был привязан SelectedIndex, поэтому добавьте

SelectedIndex="{Binding CboPeopleSelectedIndex}" 

к определению ComboBoxes.

Новый метод ResetBoundItems() выполняет обновление. Классу Person нужен Clone() метод. Я заставил ComboBox выбирать новую строку при добавлении. Это решение будет работать и для ListBoxes.

person Moonling    schedule 23.11.2017