Update DropDown listy ComboBoxa w WPF MVVM

0

Cześć,

natknąłem się w swoim projekcie na następujący problem: Dropdown list w ComboBoxie powiązanym z ObservableCollection nie zmienia się, mimo że zmieniły się właściwości wyrzucane przez ToString() obiektu tej kolekcji. Chciałbym mieć taką sytuację, że jeśli np ComboBox jest powiązany z ObservableCollection<Thing> gdzie Thing.ToString() wyrzuca Thing.Name, to w momencie edytowania wartości Name Dropdown jak i obecna wartość tego ComboBoxa aktualizują się na bieżąco z edytowaną wartością. Obecnie rozwiązałem to siłowo stosując przy setterze właściwości Name powiązanej z TextBoxem metodę RefreshList() która na na chwilę ustala wartość powiązanej ObservableCollection na null po czym ustawia ją z powrotem na poprzednią wartość - to wymusza update ComboBoxa, jednak wydaje mi się beznadziejnym rozwiązaniem jeśli chodzi o wydajność programu przy dłuższej liście. Czy jest sposób, żeby rozwiązać to ładniej?

W celu przedstawienia problemu stworzyłem prosty przykład:
https://github.com/piotr-napadlek/MVVMEditComboBoxItemsSample

A oto kod przykładu:
MainWindow.xaml

<Window x:Class="MVVMEditComboBoxItemsSample.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:MVVMEditComboBoxItemsSample"
        mc:Ignorable="d"
        Title="MVVM ComboBox edit sample" Height="372.52" Width="415.214">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="50*"></RowDefinition>
            <RowDefinition Height="81*"/>
            <RowDefinition Height="26*"/>
            <RowDefinition Height="45*"/>
            <RowDefinition Height="26*"/>
            <RowDefinition Height="44*"/>

        </Grid.RowDefinitions>
        <ComboBox Grid.Row="0" Margin="10" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" ItemsSource="{Binding Path=Things}" SelectedItem="{Binding Path=SelectedThing, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"></ComboBox>
        <StackPanel Grid.Row="1" Margin="10">
            <Button Command="{Binding Path=AddCommand}">Add Item</Button>
            <Button Command="{Binding Path=CloneCommand}">Clone Item</Button>
            <Button Command="{Binding Path=DeleteCommand}">Delete Item</Button>
        </StackPanel>
        <Label Grid.Row="2" Margin="0"  >Name</Label>
        <TextBox Grid.Row="3" HorizontalAlignment="Stretch" Margin="10"  VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Text="{Binding Path=Name, UpdateSourceTrigger=PropertyChanged}"/>
        <Label Grid.Row="4" Margin="0"  >Price</Label>
        <TextBox Grid.Row="5" HorizontalAlignment="Stretch" Margin="10" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Text="{Binding Path=Price}"/>

    </Grid>
</Window>

MainViewModel.cs

using MVVMEditComboBoxItemsSample.MockModel;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;

namespace MVVMEditComboBoxItemsSample
{
    class MainViewModel : ViewModelBase
    {
        private ObservableCollection<Thing> things;
        private Thing selectedThing;

        private ICommand addCommand;
        private ICommand cloneCommand;
        private ICommand deleteCommand;

        public MainViewModel()
        {
            Things = new ObservableCollection<Thing>(ThingDataManager.Instance.GetThings());
            SelectedThing = Things.FirstOrDefault();
        }

        public ObservableCollection<Thing> Things
        {
            get
            {
                return things;
            }

            set
            {
                things = value;
                OnPropertyChanged(nameof(Things));
            }
        }

        public Thing SelectedThing
        {
            get
            {
                return selectedThing;
            }

            set
            {
                selectedThing = value;
                OnPropertyChanged(nameof(SelectedThing));
                OnPropertyChanged(nameof(Name));
                OnPropertyChanged(nameof(Price));
            }
        }

        public string Name
        {
            get
            {
                if (SelectedThing != null)
                {
                    return SelectedThing.Name;
                }
                return null;
            }

            set
            {
                SelectedThing.Name = value;
                OnPropertyChanged(nameof(Name));
                RefreshList();
            }
        }

        public string Price
        {
            get
            {
                if (SelectedThing != null)
                {
                    return SelectedThing.Price;
                }
                return null;
            }

            set
            {
                SelectedThing.Price = value;
                OnPropertyChanged(nameof(Price));
            }
        }

        public ICommand AddCommand
        {
            get
            {
                if(addCommand==null)
                {
                    addCommand = new CommandBase(i => AddItem(), null);
                }
                return addCommand;
            }
        }

        public ICommand CloneCommand
        {
            get
            {
                if(cloneCommand==null)
                {
                    cloneCommand = new CommandBase(i => CloneItem(), i => SelectedThing!=null);
                }
                return cloneCommand;
            }
        }

        public ICommand DeleteCommand
        {
            get
            {
                if(deleteCommand==null)
                {
                    deleteCommand = new CommandBase(i => DeleteItem(), i => SelectedThing!=null);
                }
                return deleteCommand;
            }
        }

        public void AddItem()
        {
            Thing newThing = new Thing();
            Things.Add(newThing);
            SelectedThing = newThing;
        }

        public void CloneItem()
        {
            Thing clonedThing = new Thing();
            clonedThing.Name = SelectedThing.Name + " - copy";
            clonedThing.Price = SelectedThing.Price;
            Things.Add(clonedThing);
            SelectedThing = clonedThing;
        }

        public void DeleteItem()
        {
            Thing tempThing = new Thing();
            tempThing = SelectedThing;
            if (Things.IndexOf(SelectedThing) != 0)
            {
                SelectedThing = Things.FirstOrDefault();
            }
            else if (Things.Count==1)
            {
                SelectedThing = null;
            }
            else
            {
                SelectedThing = Things[1];
            }

            Things.Remove(tempThing);
        }

        private void RefreshList()
        {
            List<Thing> tempThings = Things.ToList();
            Thing tempThing = SelectedThing;
            Things = null; //for instant combobox update, comment out if unnecesary
            Things = new ObservableCollection<Thing>(tempThings);
            SelectedThing = tempThing;
        }
    }
}
 

Thing.cs

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MVVMEditComboBoxItemsSample.MockModel
{
    class Thing
    {
        public string Name { get; set; }
        public string Price { get; set; }

        public override string ToString()
        {
            return Name;
        }
    }
}

Reszta mam nadzieje jest samoopisująca się ;).

EDIT: Tzn. Zdaje sobie sprawę z tego, że update właściwości jakiegoś obiektu nie powoduje de facto zmiany kolekcji zawierającej te obiekty, bo ona nadal zawiera te same referencje, ale myślałem że wymuszenie OnPropertyChanged dla tej listy mimo wszystko wymusi przejechanie jeszcze raz po ToString() obiektów tej kolekcji.

1

A gdzie masz OnPropertyChanged na właściwości Name w klasie Thing? Nic dziwnego że się nie zmienia skoro tego nie wołasz.

Zaś w ComboBox ustaw DisplayMemberPath="Name"

0

Dzięki za odpowiedź. DisplayMemberPath w ComboBoxie ustawione - faktycznie teraz nie muszę dawać ToString() w klasie Thing. Jeśli chodzi o OnPropertyChanged we właściwości Name, to masz na myśli klasę MainViewModel? Bo klasa Thing jako klasa modelu nie powinna raczej implementować INotifyPropertyChanged? Jeśli tak, to dawanie OnPropertyChanged(nameof(Things)) nie update'uje dalej ComboBoxa - próbowałem tak wcześniej i objaw był taki sam, zmiany są zapamiętywane, ale DropDown lista jest dalej bez zmian. Kod po zmianie:

<ComboBox Grid.Row="0" Margin="10" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" ItemsSource="{Binding Path=Things}" SelectedItem="{Binding Path=SelectedThing, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" DisplayMemberPath="Name"></ComboBox>

i właściwość Name:

        public string Name
        {
            get
            {
                if (SelectedThing != null)
                {
                    return SelectedThing.Name;
                }
                return null;
            }

            set
            {
                SelectedThing.Name = value;
                OnPropertyChanged(nameof(Name));
                OnPropertyChanged(nameof(Things));
                //RefreshList();
            }
        } 
1

Wystarczy zrobić:

public ObservableCollection<Thing> Things { get; set; }

public Thing SelectedThing
{
   get { return selectedThing; }

   set
   {
      selectedThing = value;
      OnPropertyChanged(nameof(SelectedThing));
   }
}

Po stronie XAML w TextBoxach:

Text="{Binding Path=SelectedThing.Name}"
Text="{Binding Path=SelectedThing.Price}"

Metoda RefreshList() oraz propery Name oraz Price w klasie MainViewModel są zbędne.

1
fisheye_ napisał(a):

Jeśli chodzi o OnPropertyChanged we właściwości Name, to masz na myśli klasę MainViewModel?

Zacytuję siebie...

UnlimitedPL napisał(a):

A gdzie masz OnPropertyChanged na właściwości Name w klasie Thing?

Teraz powtórzę jeszcze raz. Mam nadzieję że ostatni i zrozumiesz o co chodzi.
Chodzi mi o klasę Thing bo przecież tam masz właściwość Name. LOL? Po co Ci właściwość Name w MainViewModel???

fisheye_ napisał(a):

Bo klasa Thing jako klasa modelu nie powinna raczej implementować INotifyPropertyChanged?

Czemu nie? Jest to ViewModel dla kontrolki ComboBox. Jeśli chcesz korzystać z interfejsu INotifyPropertyChanged to robisz dziedziczenie po ViewModelBase.

klasa Thing powinna wyglądać tak:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace MVVMEditComboBoxItemsSample.MockModel
{
    class Thing : ViewModelBase
    {

private string _Name;
public string Name
{
get {return _Name; }
set { _Name = value; OnPropertyChanged(nameof(Name)); }
}

        public string Price { get; set; }
    }
}

I wywal tą właściwość Name z MainViewModel bo to jakaś pomyłka...

0

OK, wszystko jasne, wszystko działa. Źle zinterpretowałem niektóre zasady mvvm i na etapie implementacji wzorca zbytnio skomplikowałem sobie życie. Obaj udzieliliście dobrych rad, więc wielkie dzięki dla Was!

1 użytkowników online, w tym zalogowanych: 0, gości: 1