Quando in una settimana due persone pongono lo stesso quesito è sintomo che quel particolare concetto non è ben chiaro o semplicemente non è stato spiegato correttamente. La domanda è: “Usando Model View ViewModel voglio creare una listbox con i vari elementi e un pulsante che mi permetta di cancellare quel determinato elemento” in pratica qualcosa tipo:
Normalmente il ViewModel che viene associato alla Window espone il comando di cancellazione della persona e la collezione di elementi da elencare, in breve qualcosa tipo:
1: public class ViewModel : ViewModelBase
2: {
3: public ViewModel()
4: {
5: this.DeletePersonCommand = new RelayCommand<object>(o =>
6: {
7: ICollectionView view = CollectionViewSource.GetDefaultView(this.Persons);
8: this.Persons.Remove((Person)view.CurrentItem);
9: });
10:
11: //Creates some persons...
12: this.Persons = new ObservableCollection<Person>()
13: {
14: new Person(){ FirstName="Corrado", LastName="Cavalli"},
15: new Person(){ FirstName="Laurent", LastName="Bugnion"},
16: new Person(){ FirstName="Marlon", LastName="Grech"},
17: new Person(){ FirstName="Josh", LastName="Smith"}
18: };
19: }
20:
21:
22: private ObservableCollection<Person> persons = new ObservableCollection<Person>();
23:
24: /// <summary>
25: /// Gets the Persons property.
26: /// Changes to that property's value raise the PropertyChanged event.
27: /// This property's value is broadcasted by the Messenger's default instance when it changes.
28: /// </summary>
29: public ObservableCollection<Person> Persons
30: {
31: get
32: {
33: return this.persons;
34: }
35:
36: private set
37: {
38: if (this.persons != value)
39: {
40: this.persons = value;
41: RaisePropertyChanged("Persons");
42: }
43: }
44: }
45:
46: public RelayCommand<object> DeletePersonCommand { get; private set; }
47:
48: }
mentre la UI è definita secondo questo XAML:
1: <Window x:Class="LaurentToolkit.MainWindow"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
5: xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
6: mc:Ignorable="d"
7: Height="355"
8: Width="531"
9: DataContext="{Binding Main, Source={StaticResource Locator}}"
10: Title="{Binding WindowTitle}"
11: >
12: <Window.Resources>
13: <DataTemplate x:Key="PersonTemplate">
14: <StackPanel >
15: <TextBlock Text="{Binding FirstName, Mode=Default}" />
16: <TextBlock Text="{Binding LastName, Mode=Default}" />
17: <Button Content="Delete"
18: Command="{Binding DeletePersonCommand}" />
19: </StackPanel>
20: </DataTemplate>
21: </Window.Resources>
22: <Grid>
23: <Button HorizontalAlignment="Right" Margin="0,109,48,0" Width="72" Content="Close" Command="{Binding QuitCommand, Mode=Default}" Height="40" VerticalAlignment="Top" />
24: <ListBox HorizontalAlignment="Left" Margin="33,12,0,24" Width="173" ItemsSource="{Binding Persons, Mode=Default}" ItemTemplate="{DynamicResource PersonTemplate}"/>
25: </Grid>
26: </Window>
Eseguendo il tutto, premendo il pulsante il comando DeletePersonCommand non viene invocato e il motivo è facilmente identificabile dando un occhio alla output window di Visual Studio che inesorabilemente riporta:
indicando chiaramente che il meccanismo di binding si aspetta di trovare la proprietà nell’oggetto associato al DataTemplate il quale rappresenta a tutti gli effetti il DataContext associato a ogni elemento della listbox.
Per far funzionare il tutto è necessario risalire al DataContext “principale” ereditato dai vari controlli inclusa la listbox, per far questo ci sono svariate possibilità, la più immediata, sebbene la sintassi da utilizzare non lo sia affatto, è quella di utilizzare un Binding relativo, risalendo il logical tree fino ad incontrare la listbox che contiene le istanze dei vari DataTemplates.
Modifichiamo il DataTemplate in:
1: <DataTemplate x:Key="PersonTemplate">
2: <StackPanel >
3: <TextBlock Text="{Binding FirstName, Mode=Default}" />
4: <TextBlock Text="{Binding LastName, Mode=Default}" />
5: <Button Content="Delete"
6: Command="{Binding RelativeSource={RelativeSource FindAncestor,
7: AncestorLevel=1,
8: AncestorType={x:Type ListBox}},
9: Path=DataContext.DeletePersonCommand}" />
10: </StackPanel>
11: </DataTemplate>
e il tutto funziona egregiamente.
Quello che abbiamo realizzato mediante RelativeSource è ‘risalire’ la gerarchia di elementi fino a raggiungere la listbox per poi usare come sorgente di binding la properietà DeletePersonCommand che è raggiungibile attraverso la proprietà DataContext della listbox la quale, grazie al DataContext inheritance, è stato valorizzato con l’istanza del ViewModel associato alla Window.
Questo approccio non è compatibile con Silverlight in quanto sebbene la versione 3.0 supporti RelativeSource questa è limitata alle opzioni Self e TemplatedParent, una possibile soluzione è quella di dare un nome alla listbox e usare un altra novità presente nella versione 3.0 ovvero ElementName, in pratica qualcosa tipo:
1: <Window.Resources>
2: <DataTemplate x:Key="PersonTemplate">
3: <StackPanel >
4: <TextBlock Text="{Binding FirstName, Mode=Default}" />
5: <TextBlock Text="{Binding LastName, Mode=Default}" />
6: <Button Content="Delete"
7: Command="{Binding ElementName=lb, Path=DataContext.DeletePersonCommand}" />
8: </StackPanel>
9: </DataTemplate>
10: </Window.Resources>
11: <Grid>
12: <Button HorizontalAlignment="Right" Margin="0,109,48,0" Width="72" Content="Close" Command="{Binding QuitCommand, Mode=Default}" Height="40" VerticalAlignment="Top" />
13: <ListBox x:Name="lb" HorizontalAlignment="Left" Margin="33,12,0,24" Width="173" ItemsSource="{Binding Persons, Mode=Default}" ItemTemplate="{DynamicResource PersonTemplate}"/>
14: </Grid>