2012-09-28 11 views
5

Sto utilizzando un'applicazione standard wpf/mvvm in cui associo le caselle combinate alle raccolte su un ViewModel.Deseleziona ComboBoxItems in MVVM

Devo essere in grado di deselezionare un elemento dal menu a discesa. Significa che gli utenti dovrebbero essere in grado di selezionare qualcosa, e in seguito decidere di deselezionarlo (selezionarne uno). il problema è che non ci sono elementi vuoti nella mia collezione rilegata

il mio pensiero iniziale era semplicemente quello di inserire un nuovo oggetto nella collezione che risultasse avere un oggetto vuoto sopra la collezione.

questo è un trucco, e interessa tutto il codice che utilizza tale raccolta sul modello di visualizzazione.

per esempio se qualcuno è stato quello di scrivere

_myCollection.Frist(o => o.Name == "foo") 

questo sarà un'eccezione di riferimento null.

possibile soluzione è:

_myCollection.Where(o => o != null).First(o => o.Name == "foo"); 

questo funzionerà, ma c'è modo di garantire eventuali futuri utilizzi di tale raccolta non causerà interruzioni.

qual è un buon schema/soluzione per poter aggiungere un elemento vuoto in modo che l'utente possa deselezionare. (Sono anche consapevole della struttura CollectionView, ma che sembra come un eccessivo per una cosa così semplice)

Aggiornamento

andò con @hbarck suggerimento e implementato CompositeCollection (Quick Proof of concept)

public CompositeCollection MyObjects { 
     get { 
      var col = new CompositeCollection(); 

      var cc1 = new CollectionContainer(); 
      cc1.Collection = _actualCollection; 

      var cc2 = new CollectionContainer(); 
      cc2.Collection = new List<MyObject>() { null }; // PROBLEM 

      col.Add(cc2); 
      col.Add(cc1); 
      return col; 
     } 
    } 

questo codice funziona con i collegamenti esistenti (incluso SelectedItem) che è ottimo.

Un problema con questo è, che se l'elemento è completamente null, il setter SelectedItem non viene mai chiamato selezionando.

Se modifico che una linea a questo:

  cc2.Collection = new List<MyObject>() { new MyObject() }; // PROBLEM 

il setter è chiamato, ma ora la mia voce selezionata è solo una classe base inizializzata invece di null .. Ho potuto aggiungere un po 'di codice nel setter per controlla/ripristina, ma non va bene.

+2

si potrebbe consentire all'utente di de-selezionare una voce facendo clic su un MenuItem in un menu contestuale. –

+0

clic destro sul menu a discesa? non è abbastanza intuitivo .. –

+0

Potrei vivere con quello. ^^ Il Combobox non si apre nemmeno se si fa clic con il tasto destro su di esso, quindi non sarebbe ostile all'utente. –

risposta

3

Penso che il modo più semplice sarebbe utilizzare un CompositeCollection. Basta aggiungere la tua raccolta a un'altra raccolta che contiene solo l'oggetto vuoto (null o un oggetto segnaposto, qualunque sia la suite di cui hai bisogno) e rendere CompositeCollection l'oggetto ItemsSource per il ComboBox. Questo è probabilmente ciò a cui è destinato.

Aggiornamento:

Questo risulta essere più complicato di quanto ho pensato, ma in realtà, mi si avvicinò con questa soluzione:

<Window x:Class="ComboBoxFallbackValue" 
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
xmlns:t="clr-namespace:TestWpfDataBinding" 
    xmlns:s="clr-namespace:System;assembly=mscorlib" 
    xmlns:w="clr-namespace:System.Windows;assembly=WindowsBase" 
Title="ComboBoxFallbackValue" Height="300" Width="300"> 
<Window.Resources> 
    <t:TestCollection x:Key="test"/> 
    <CompositeCollection x:Key="MyItemsSource"> 
     <x:Static Member="t:TestCollection.NullItem"/> 
     <CollectionContainer Collection="{Binding Source={StaticResource test}}"/> 
    </CompositeCollection> 
    <t:TestModel x:Key="model"/> 
    <t:NullItemConverter x:Key="nullItemConverter"/> 
</Window.Resources> 
<StackPanel> 
    <ComboBox x:Name="cbox" ItemsSource="{Binding Source={StaticResource MyItemsSource}}" IsEditable="true" IsReadOnly="True" Text="Select an Option" SelectedItem="{Binding Source={StaticResource model}, Path=TestItem, Converter={StaticResource nullItemConverter}, ConverterParameter={x:Static t:TestCollection.NullItem}}"/> 
    <TextBlock Text="{Binding Source={StaticResource model}, Path=TestItem, TargetNullValue='Testitem is null'}"/> 
</StackPanel> 

In sostanza, il modello è che si dichiara un singleton NullInstance della classe che si utilizza come elementi e si utilizza un convertitore che converte questa istanza in null quando si imposta la proprietà VM. Il convertitore può essere scritto universalmente, come questo (è VB, spero che non ti dispiaccia):

Public Class NullItemConverter 
Implements IValueConverter 

Public Function Convert(value As Object, targetType As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.Convert 
    If value Is Nothing Then 
     Return parameter 
    Else 
     Return value 
    End If 
End Function 

Public Function ConvertBack(value As Object, targetType As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.ConvertBack 
    If value Is parameter Then 
     Return Nothing 
    Else 
     Return value 
    End If 
End Function 

End Class

Dal momento che è possibile riutilizzare il convertitore, è possibile impostare questo tutto in XAML ; l'unica cosa che rimane da fare nel codice è fornire il singleton NullItem.

+0

mi piace questo. la mia unica preoccupazione qui è che io uso SelectedItem Binding .. che deve essere lo stesso oggetto (non clonato) che è nella collezione originale, affinché la selezione funzioni correttamente. –

+0

Se si rende nulla l'elemento "non selezionato", penso che questo non sarà un problema. Gli elementi non saranno clonati, per quanto ne so, quindi l'oggetto SelectedItem sarà un membro della raccolta o null. – hbarck

+1

ha implementato CompositeCollection, ma ha riscontrato un comportamento WPF interessante .. (ha aggiornato la mia domanda) –

0

Un'opzione potrebbe essere quella di creare una raccolta di adattatori che esponi specificamente per i consumatori che desiderano un elemento iniziale "vuoto". Dovresti creare una classe wrapper che implementi IList (se vuoi le stesse prestazioni di ObservableCollection) e INotifyCollectionChanged. Dovresti ascoltare INotifyCollectionChanged nella raccolta incartata, quindi ritrasmettere gli eventi con gli indici spostati di uno. Tutti i metodi di elenco pertinenti dovrebbero anche spostare gli indici di uno.

public sealed class FirstEmptyAdapter<T> : IList<T>, IList, INotifyCollectionChanged 
{ 
    public FirstEmptyCollection(ObservableCollection<T> wrapped) 
    { 
    } 

    //Lots of adapter code goes here... 
} 

minimo indispensabile se si vuole evitare i metodi IList è quello di implementare INotifyCollectionChanged e IEnumerable<T>.

1

È anche possibile estendere il ComboBox per abilitare la deselezione.Aggiungi uno o più hook (ad esempio, premendo il tasto Esc) per consentire all'utente di impostare il valore SelectedItem su null.

using System.Windows.Input; 

public class NullableComboBox : ComboBox 
{ 
    public NullableComboBox() 
     : base() 
    { 
     this.KeyUp += new KeyEventHandler(NullableComboBox_KeyUp); 

     var menuItem = new MenuItem(); 
     menuItem.Header = "Remove selection"; 
     menuItem.Command = new DelegateCommand(() => { this.SelectedItem = null; }); 
     this.ContextMenu = new ContextMenu(); 
     this.ContextMenu.Items.Add(menuItem); 
    } 

    void NullableComboBox_KeyUp(object sender, System.Windows.Input.KeyEventArgs e) 
    { 
     if (e.Key == Key.Escape || e.Key == Key.Delete) 
     { 
      this.SelectedItem = null; 
     } 
    } 
} 

Modifica appena notato il commento di Florian GI, il menu contestuale potrebbe essere un altro gancio buona deselezionare da aggiungere.

+0

Da dove prelevo DelegateCommand? Come si usa questo all'interno di un AXML invece di un ComboBox? – theAlse

+0

@theAlse ad esempio: https://code.google.com/p/moltenmonkey/source/browse/Source/mmonkey/Commands/DelegateCommand.cs?spec = svneb38ee7524ec4ec9cf2520ddee47ad303396e389 & r = eb38ee7524ec4ec9cf2520ddee47ad303396e389 – McGarnagle

+0

che usa 'Command', dove lo prendo? – theAlse

2

Personalmente, tendo ad aggiungere una versione "vuota" dell'oggetto che è nella mia collezione a cui sono vincolato. Quindi, ad esempio, se stai vincolando a un elenco di stringhe, nel tuo viewmodel, inserisci una stringa vuota all'inizio della raccolta. Se il tuo modello ha la raccolta di dati, quindi avvolgila con un'altra raccolta nel tuo viewmodel.

MODELLO:

public class Foo 
{ 
    public List<string> MyList { get; set;} 
} 

vista del modello:

public class FooVM 
{ 
    private readonly Foo _fooModel ; 

    private readonly ObservableCollection<string> _col; 
    public ObservableCollection<string> Col // Binds to the combobox as ItemsSource 
    { 
     get { return _col; } 
    } 

    public string SelectedString { get; set; } // Binds to the view 

    public FooVM(Foo model) 
    { 
     _fooModel = model; 
     _col= new ObservableCollection<string>(_fooModel.MyList); 
     _col.Insert(0, string.Empty); 
    } 
} 
0

Un approccio semplice è quello di ri-modello ComboBox in modo che quando v'è un elemento selezionare compare una piccola X sul lato destro della la scatola. Facendo clic si cancella l'elemento selezionato.

Questo ha il vantaggio di non fare le vostre ViewModels più complicata

+0

Forse usi un Adorner? – McGarnagle

Problemi correlati