2010-02-09 15 views
6

Rozważmy następujący XAML:resetuje WPF ComboBox zaznaczony element, gdy element source zmienia

<ComboBox Name="CompanyComboBox" 
    HorizontalAlignment="Stretch" 
    ItemsSource="{Binding Path=GlobalData.Companies}" 
    SelectedValuePath="Id" 
    SelectedValue="{Binding Customer.CompanyId, ValidatesOnDataErrors=True}" 
    DisplayMemberPath="Name" /> 

GlobalData.Companies jest zbiorem (IEnumerable<Company>) przedsiębiorstwa; kolekcja ta może zostać ponownie załadowana w tle (jest pobierana z serwisu internetowego). Gdy tak się stanie, ComboBox poprawnie załaduje elementy poprzez powiązanie. Jednak jako efekt uboczny, resetuje również wybrany element!

Użyłem reflektora do sprawdzenia źródeł combo-box i najwyraźniej jest to zamierzone zachowanie.

Czy jest jakiś "miły" sposób obejścia tego? To, co chcę osiągnąć, to to, że jeśli użytkownik wybierze "Firmę A" i ponownie załaduje listę firm, wówczas "Firma A" pozostanie wybrana (zakładając, że znajduje się na nowej liście).

Odpowiedz

4

Może możesz użyć numeru ObservableCollection<Company> zamiast swojego IEnumerable<Company>? Następnie, w przypadku zmiany tła, tylko Dodaj/Usuń pozycje, które są nowe/nieobecne na nowej liście, wybrany element powinien pozostać, chyba że został usunięty przez zmianę.

Możesz update your observable collection in a separate thread with a small hack-around.

+0

To może być rozwiązanie, chociaż nie jest tak ładne/łatwe, jak chciałbym: - | –

+0

Prawdopodobnie nie trzeba aktualizować kolekcji z wątku tła. Pobierz bieżącą kolekcję z usługi sieciowej w tle, a następnie (zakładając, że korzystasz z usługi BackgroundWorker) zaktualizuj 'ObservableCollection ' w zdarzeniu RunWorkerCompleted. –

0

hmm, nie wiem, czy jest to "przyjemny" sposób, ale jeśli możesz uzyskać dostęp do wybranego elementu przed ponownym załadowaniem, możesz go zapisać (lub jego klucz lub coś) i ponownie wybrać programowo po zakończeniu ładowania.

szybkie makieta:

var selectedItem = myCombo.SelectedItem; 
DoReload(); 
myCombo.SelectedItem = selectedItem; 

Ale zakładam, że myśli w inny sposób niż ten ręcznej pracy wokół?
Nadzieja pomaga to w każdym razie ...

UPDATE
Ok widzę, z wątku tła.
Czy używasz ICollectionView do wiązania swojego combobox? Jeśli tak, możesz użyć właściwości CurrentItem, aby zachować referencję. Zrobiłem szybką makietę i to działa na mojej konfiguracji. to zakłada masz odniesienie do swojej Ui:

XAML

<Grid VerticalAlignment="Top"> 
    <Grid.ColumnDefinitions> 
     <ColumnDefinition /> 
     <ColumnDefinition /> 
    </Grid.ColumnDefinitions> 
    <ComboBox ItemsSource="{Binding Items}" IsSynchronizedWithCurrentItem="True" Grid.Column="0" Grid.Row="0" DisplayMemberPath="Name"/> 
    <Button Command="{Binding UpdateCommand}" Grid.Column="1" Grid.Row="0">Update</Button> 
</Grid> 

View/ViewModel

public partial class Window1 : Window { 
    public Window1() { 
     InitializeComponent(); 
     this.DataContext = new ViewModel(this); 
    } 
} 

public class ViewModel 
{ 
    private readonly Window1 window; 
    private ObservableCollection<Item> items; 
    private ICollectionView view; 

    public ViewModel(Window1 window) { 
     this.window = window; 
     items = new ObservableCollection<Item> 
      { 
       new Item("qwerty"), 
       new Item("hello"), 
       new Item("world"), 
      }; 

     view = CollectionViewSource.GetDefaultView(items); 
    } 

    public ObservableCollection<Item> Items { get { return items; } } 

    public ICommand UpdateCommand { 
     get { return new RelayCommand(DoUpdate); } 
    } 

    public Item SelectedItem { get; set; } 

    private void DoUpdate(object obj) { 
     var act = new Func<List<Item>>(DoUpdateAsync); 
     act.BeginInvoke(CallBack, act); 
    } 

    private List<Item> DoUpdateAsync() { 
     return new List<Item> { 
       new Item("hello"), 
       new Item("world"), 
       new Item("qwerty"), 
      }; 
    } 

    private void CallBack(IAsyncResult result) { 
     try { 
      var act = (Func<List<Item>>)result.AsyncState; 
      var list = act.EndInvoke(result); 

      window.Dispatcher.Invoke(new Action<List<Item>>(delegate(List<Item> lst) { 
                    var current = lst.Single(i => i.Name == ((Item)view.CurrentItem).Name); 
                    Items.Clear(); 
                    lst.ForEach(Items.Add); 
                    view.MoveCurrentTo(current); 
                   }), list); 

     } catch(Exception exc){ Debug.WriteLine(exc); } 
    } 
} 

public class Item { 
    public Item(string name) { 
     Name = name; 
    } 
    public string Name { get; set; } 
} 

Trzeba będzie zrobić jakąś manipulację w przypadku, gdy wybrana pozycja nie jest dłużej na liście.
Właściwość IsSynchronizedWithCurrentItem jest ważna tutaj, w przeciwnym razie nie będzie działać!
Sposób, w jaki tworzone jest odniesienie do głównego okna, powinien być również oparty na architekturze DI.

+0

Problem polega na tym, że DoReload() jest wykonywane w tle przez klasę, która nie ma pojęcia o GUI. GUI jest powiadamiany o nowych wartościach poprzez Binding i INotifyPropertyChanged. –

0

Jak zauważył Yacoder, ma to związek z równością obiektów. Dopóki bindujesz SelectedValue zamiast SelectedItem możesz zdefiniować ItemsSource jako kolekcję anonimowych typów. Wtedy ten problem nie wystąpi (i jest również szybszy, jeśli musisz odczytać wartości z bazy danych).

Powiązane problemy