Update DropDown listy ComboBoxa w WPF MVVM

Update DropDown listy ComboBoxa w WPF MVVM
FI
  • Rejestracja:około 10 lat
  • Ostatnio:ponad 7 lat
  • Postów:11
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

Kopiuj
<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

Kopiuj
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

Kopiuj
 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.

edytowany 1x, ostatnio: fisheye_
UnlimitedPL
  • Rejestracja:prawie 12 lat
  • Ostatnio:około 6 lat
  • Postów:231
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"

edytowany 1x, ostatnio: UnlimitedPL
FI
  • Rejestracja:około 10 lat
  • Ostatnio:ponad 7 lat
  • Postów:11
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:

Kopiuj
<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:

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

            set
            {
                SelectedThing.Name = value;
                OnPropertyChanged(nameof(Name));
                OnPropertyChanged(nameof(Things));
                //RefreshList();
            }
        } 
DibbyDum
  • Rejestracja:ponad 12 lat
  • Ostatnio:ponad rok
  • Lokalizacja:Polska, Kraków
1

Wystarczy zrobić:

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

public Thing SelectedThing
{
   get { return selectedThing; }

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

Po stronie XAML w TextBoxach:

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

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


Yubby dibby dibby dibby dibby dibby dibby dum..
UnlimitedPL
  • Rejestracja:prawie 12 lat
  • Ostatnio:około 6 lat
  • Postów:231
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:

Kopiuj
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...

edytowany 4x, ostatnio: UnlimitedPL
T2
Hey UnlimitedPL, mam pytanko. Jeżeli masz obiekt DTo który służy do komunikacji pomiedzy UI a Serwisem. Czy zawsze tworzysz jakąś klasę dedykowaną dla UI która implementuje inotifypropertychanged czy rozszerzesz DTO ?
UnlimitedPL
@teo215 Chodzi ci o obiekty które są przysyłane z serwisu? Nie zawsze. Tworzę klasę dedykowaną po stronie aplikacji klienckiej wtedy kiedy jest to mi do czegoś potrzebne.
FI
  • Rejestracja:około 10 lat
  • Ostatnio:ponad 7 lat
  • Postów:11
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!

Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)