2017-03-09 13 views
6

Mam ListBox że ma swoje ItemsSource związany z klasy niestandardowej, która (prawidłowo) implementuje INotifyCollectionChanged i SelectedItem związany z polem w ViewModel.wybór Usuń gdy wybrana pozycja zostanie usunięta z pola listy

Problem polega na tym, że po usunięciu aktualnie SelectedItem z kolekcji ItemsSource natychmiast zmienia ona wybór na sąsiedni element. Bardzo bym wolał, gdyby po prostu usunięto selekcję.

Powodem, dla którego jest to dla mnie problemem jest podążanie. Klasa ItemsSource zawiera elementy z innej kolekcji, które albo spełniają pewne predykaty (podczas stałej czasu wykonywania), albo są Active. Bycie Active jest "zsynchronizowane" z byciem SelectedItem (istnieją ku temu powody). Jest więc bardzo możliwe, że dany przedmiot może być dozwolony tylko w przypadku, gdy jest wybrany, co oznacza, że ​​może on zniknąć, gdy użytkownik wybierze inny element.

Moja funkcja (głęboko w „modelu”), która jest wywoływana gdy SelectedItem ulega zmianie:

//Gets old Active item 
var oldActiveSchema = Schemas.FirstOrDefault(sch => sch.IsActive); 

//Makes the new Item active (which triggers adding it into `ItemsSource` in case it didn't satisfy the Predicate) 
((PowerSchema)newActiveSchema).IsActive = true; 
//Triggers PropertyChanged on ViewModel with the new Active item 
CurrentSchema = newActiveSchema; 
RaisePropertyChangedEvent(nameof(CurrentSchema)); (#1) 

//Changes the old item so it stops being Active -> gets removed from `ItemsSource` (#2) 
if (oldActiveSchema != null) { ((PowerSchema)oldActiveSchema).IsActive = false; } 

Problem jest, że z jakiegoś powodu aktualizację ListBox ze względu na zmianę SelectedItem który powinien uzyskać wyzwolone przez (# 1) zostaje odroczone (wiadomość o aktualizacji ListBox prawdopodobnie kończy się pętlą wiadomości WPF i czeka tam do chwili zakończenia obecnego obliczania).

Usunięcie oldActiveSchema z ItemsSource, z drugiej strony, jest natychmiastowe, a także natychmiast wyzwala zmianę SelectedItem do jednego, który jest obok starego (jeśli usunąć wybraną pozycję, sąsiedni zostanie wybrany zamiast) . A ponieważ zmiana SelectedItem uruchamia moją funkcję, która ustawia CurrentSchema na niewłaściwy (sąsiedni) element, przepisuje on wybrany przez użytkownika CurrentSchema (nr 1) i do czasu, kiedy wiadomość o aktualizacji ListBox spowodowana PropertyChanged zostanie uruchomiona, po prostu zaktualizuje ją do sąsiedni.

Każda pomoc jest bardzo doceniana.


rzeczywisty kod, jeśli ktoś chciałby kopać głębiej:

  • ListBox
  • ViewModel
  • The model's method
  • Callstack kiedy sąsiedni element zostanie wybrany jako SelectedItem zamiast jednego użytkownika wybrał
    • przewód 46: the SelectedItem wybrany przez użytkownika wkracza w sposób jako taki, który ma się uzyskać aktywny
    • linia 45: stare SelectedItem przestanie być aktywny -> usuwany jest ze zbioru (44-41)
    • linia 32: MoveCurrencyOffDeletedElement porusza SelectedItem
    • linia 5: SelectedItem zostaje zmieniona na sąsiedni jeden
+0

Usuwanie zaznaczenia powinno po prostu wymagać ustawienia właściwości SelectedItem w twoim ViewModel na wartość null.Ponieważ problem polega na zmianie schematu, wystarczy zapisać wybranyitem w zmiennej lokalnej, ustawić selecteditem na wartość null, a * następnie * usunąć wybrany element z kolekcji. –

+0

Wpadnij [#WPF] (https://chat.stackoverflow.com/rooms/18165/wpf) i zostaw mi ping, jeśli nie jestem aktywny na czacie. Spojrzałem na twój kod, ale nie mogę od razu dowiedzieć się, jak wywołać ten problem. Jest też sporo innych pomocnych mieszkańców, którzy mogą pomóc, jeśli jestem nieaktywny. – Maverik

+1

I rzeczywiście pomysł Brandona na zmianę wyboru jest tym, o czym myślałem. Także jeśli bindujesz Selector.IsSelected na IsActive ... Twój wybór może automatycznie podążać za flagą IsActive, nie mając do czynienia z bieżącym przedmiotem – Maverik

Odpowiedz

1

Diagnoza

Kluczem do problemów jest ustawienie IsSynchronizedWithCurrentItem="True" na swoim ListBox. To, co robi, powoduje synchronizację ListBox.SelectedItem i ListBox.Items.CurrentItem. Ponadto, ListBox.Items.CurrentItem jest zsynchronizowany z właściwością ICollectionView.CurrentItem domyślnego widoku kolekcji dla kolekcji źródłowej (ten widok jest zwracany w Twoim przypadku przez CollectionViewSource.GetDefaultView(Schemas)). Teraz po usunięciu elementu z kolekcji Schemas, która również jest CurrentItem z odpowiedniego widoku kolekcji, widok domyślnie aktualizuje jego CurrentItem do następnego elementu (lub poprzedniego, jeśli usunięty element był ostatnim, lub null, jeśli usunięty element był jedynym elementem w kolekcji).

Drugą częścią problemu jest to, że gdy ListBox.SelectedItem ulega zmianie powodując aktualizację swojej własności view-modelu, Twój RaisePropertyChangedEvent(nameof(ActiveSchema)) jest przetwarzany po proces aktualizacji zostanie zakończone, zwłaszcza po powrocie sterowania z ActiveSchema seter. Możesz zaobserwować, że getter nie jest trafiony natychmiast, ale dopiero po zakończeniu ustawiania. Co ważne, widok CurrentItem widoku Schemas również nie jest natychmiast aktualizowany, aby odzwierciedlić nowo wybrany element. Z drugiej strony, po ustawieniu IsActive = false na poprzednio wybranym elemencie, powoduje natychmiastowe "usunięcie" tego elementu z kolekcji Schemas, co z kolei powoduje aktualizację CurrentItem widoku kolekcji, a łańcuch natychmiast kontynuuje aktualizację ListBox.SelectedItem. Możesz zauważyć, że w tym momencie setter ActiveSchema zostanie trafiony ponownie. Tak więc Twoja ActiveSchema zostanie ponownie zmieniona (do pozycji obok poprzednio wybranej) jeszcze przed zakończeniem przetwarzania poprzedniej zmiany (do elementu wybranego przez użytkownika).

Rozwiązanie

Istnieje kilka sposobów, aby rozwiązać ten problem:

# 1

Ustaw IsSynchronizedWithCurrentItem="False" na ListBox (lub pozostawić nietknięte). To sprawi, że twój problem zniknie bez wysiłku. Jeśli jednak z jakiegoś powodu jest to wymagane, użyj innego rozwiązania.

# 2

Zapobieganie reentrant próby ustawić ActiveSchema stosując flagę Strażnik:

bool ignoreActiveSchemaChanges = false; 
public IPowerSchema ActiveSchema 
{ 
    get { return pwrManager.CurrentSchema; } 
    set 
    { 
     if (ignoreActiveSchemaChanges) return; 
     if (value != null && !value.IsActive) 
     { 
      ignoreActiveSchemaChanges = true; 
      pwrManager.SetPowerSchema(value); 
      ignoreActiveSchemaChanges = false; 
     } 
    } 
} 

spowoduje automatyczne aktualizacje do widoku Collection CurrentItem być ignorowane przez Pana zdaniem model , a docelowo ActiveSchema zachowa oczekiwaną wartość.

# 3

ręcznie zaktualizować widok Collection CurrentItem do nowo wybranego elementu przed „usuń” poprzednio wybrany jeden. Będziesz potrzebował odwołania do kolekcji MainWindowViewModel.Schemas, aby można było przekazać ją jako parametr do metody setNewCurrSchema lub enkapsulować kod w delegata i przekazać jako parametr. Pokażę tylko drugą opcję:

W klasie PowerManager:

//we pass the action as an optional parameter so that we don't need to update 
//other code that uses this method 
private void setNewCurrSchema(IPowerSchema newActiveSchema, Action action = null) 
{ 
    var oldActiveSchema = Schemas.FirstOrDefault(sch => sch.IsActive); 

    ((PowerSchema)newActiveSchema).IsActive = true; 
    CurrentSchema = newActiveSchema; 
    RaisePropertyChangedEvent(nameof(CurrentSchema)); 

    action?.Invoke(); 

    if (oldActiveSchema != null) 
    { 
     ((PowerSchema)oldActiveSchema).IsActive = false; 
    } 
} 

W klasie MainWindowViewModel:

public IPowerSchema ActiveSchema 
{ 
    get { return pwrManager.CurrentSchema; } 
    set 
    { 
     if (value != null && !value.IsActive) 
     { 
      var action = new Action(() => 
      { 
       //this will cause a reentrant attempt to set the ActiveSchema, 
       //but it will be ignored because at this point value.IsActive == true 
       CollectionViewSource.GetDefaultView(Schemas).MoveCurrentTo(value); 
      }); 
      pwrManager.SetPowerSchema(value, action); 
     } 
    } 
} 

Zauważ jednak, że wymaga to odniesienie do zespołu PresentationFramework. Jeśli nie chcesz tej zależności w zestawie modelu widoku, możesz utworzyć zdarzenie, które zostanie zasubskrybowane przez widok, a wymagany kod zostanie uruchomiony przez widok (który jest już zależny od zestawu PresentationFramework). Ta metoda jest często określana jako wzorzec żądania interakcji (patrz User Interaction Patterns) w sekcji Prism 5.0 Przewodnik po MSDN).

# 4

odroczyć „usuwanie” z wcześniej wybranej pozycji do aktualizacji wiązania jest zakończona. Można to osiągnąć przez kolejek kod do wykonania za pomocą Dispatcher:

private void setNewCurrSchema(IPowerSchema newActiveSchema) 
{ 
    var oldActiveSchema = Schemas.FirstOrDefault(sch => sch.IsActive); 

    ((PowerSchema)newActiveSchema).IsActive = true; 
    CurrentSchema = newActiveSchema; 
    RaisePropertyChangedEvent(nameof(CurrentSchema)); 

    if (oldActiveSchema != null) 
    { 
     //queue the code for execution 
     //in case this code is called due to binding update the current dispatcher will be 
     //the one associated with UI thread so everything should work as expected 
     Dispatcher.CurrentDispatcher.InvokeAsync(() => 
     { 
      ((PowerSchema)oldActiveSchema).IsActive = false; 
     }); 
    } 
} 

wymaga odniesienie do zespołu WindowsBase, który ponownie można uniknąć w zespole Widok modelu poprzez stosując metodę opisaną w roztworze nr 3 .

Osobiście wybrałbym rozwiązanie nr 1 lub nr 2, ponieważ zachowuje ono czystość klasy PowerManager, a 3 i 4 wydają się podatne na nieoczekiwane zachowanie.

+0

Dzięki, miałem pomysły podobne do # 3 i # 4, ale oba te rozwiązania wydawały mi się trochę "brudne". Poza tym naprawdę nie chciałem zanieczyszczać 'PowerManagera' hackami związanymi z interfejsem użytkownika. # 2 to świetny pomysł. Nie myślałem o tym, mimo że było to oczywiste. Pójdę jednak z numerem 1. W pewnym sensie źle zrozumiałem, co robi 'IsSynchronizedWithCurrentItem =' i myślałem, że jest to wymagane w moim przypadku użycia. Okazało się, że tak nie jest. – Petrroll

Powiązane problemy