Un cas classique
Si vous développez en WPF, avec le pattern MVVM, vous avez certainement déjà rencontré le cas d’une simple vue Maître/Détail : le peuplement de la vue détail se fait par le choix d’un item sur la vue maître. Lors de ce peuplement, il arrive qu’un des contrôles soit un dérivé de la classe ItemControl, comme une ListBox ou une ComboBox. Pour garder le découplage total entre l’IHM et la couche métier, MVVM nous impose de passer par une propriété exposée dans votre VueModèle associée à votre vue. Paramétrer cette propriété en mode « TwoWay » (les modifications de la cible et de la source sont toutes deux génératrices de messages) ne vous apportera pas la solution ici… Je vous offre donc un contournement simple.
Le contexte applicatif
Voici ci-dessous l’IHM très basique et non traitée encore d’un projet sur lequel je travaille (que nous publierons prochainement en tant que projet) : à droite, une vue maître liste des projets disponibles. La sélection d’un des ces projets entraîne une mise à jour de la vue détail à droite. Parmi les contrôles de cette vue détails, une Combobox liste tous les clients disponibles. Mon objectif est naturellement de peupler la Combobox avec tous les clients existants (au chargement de la vue) puis de sélectionner le client auquel le projet est affecté.
Le remplissage avec tous les clients fonctionne, il suffit pour cela d’exposer une propriété dans la VueModèle associée à la vue détail qui offre une ObservableCollection<Customer> :
public class DetailedProjectViewModel : ViewModelBase
{
...
/// /// Initializes a new instance of the DetailedProjectViewModel class. ///
public DetailedProjectViewModel()
{
// Message subscription : to subscribe to SelectedProject change on the MasterView
Messenger.Default.Register
(
this,
(action) => DispatcherHelper.CheckBeginInvokeOnUI(() => SelectedProject = action.NewValue)
);
// Get all the customer
GetAllCustomers();
}
...
/// /// The property's name. ///
public const string SelectedProjectPropertyName = "SelectedProject";
private Project _selectedProject;
/// /// Sets and gets the SelectedProject property. /// Changes to that property's value raise the PropertyChanged event. ///
public Project SelectedProject
{
get
{
return _selectedProject;
}
set
{
if (_selectedProject == value)
{
return;
}
_selectedProject = value;
RaisePropertyChanged(SelectedProjectPropertyName);
}
}
/// /// The property's name. ///
public const string AllCustomersPropertyName = "AllCustomers";
private CollectionView _allCustomersProperty;
/// /// Sets and gets the AllCustomers property. /// Changes to that property's value raise the PropertyChanged event. ///
public CollectionView AllCustomersProperty
{
get
{
return _allCustomersProperty;
}
set
{
if (_allCustomersProperty == value)
{
return;
}
_allCustomersProperty = value;
RaisePropertyChanged(AllCustomersPropertyName);
}
}
private void GetAllCustomers()
{
var getAllCustomers = from p in _context.CustomerJeu
select p;
AllCustomersProperty = new CollectionView(getAllCustomers);
}
}
puis de la lier à la source de Combobox :
Définir l’item actif par le code
Pour pouvoir avoir l’Item sélectionné se mettant à jour dans la ComboBoxBox, le fonctionnement « normal » en MVVM (Modèle Vue Vue-Modèle, le pattern de référence pour développer avec WPF) aurait été le suivant :
- Exposer une propriété (ou en utiliser une d’un type contenant un item de la ComboBox, ce que je fais pour limiter le code) en mode TwoWay.
- Lier cette propriété à l’attribut ItemSelected de la ComboBox
Ainsi, le mode TwoWay devrait pouvoir propager une modification de la propriété cible (l’utilisateur qui choisit un autre client dans la ComboBox) tout comme de la propriété source (le code met à jour la propriété suite au changement de projet actif). Seulement voilà, ça ne fonctionne pas comme ça, puisque pour déterminer quel Item de la ComboBox est actif, le framework va utiliser la méthode Equals() d’un des types racine de .NET : object. Cette méthode fonctionne par référence et non pas par valeur, ce qui veut dire qu’elle va tester si les 2 instances passées en paramètres pointent sur le même objet et non pas si les 2 objets ont un contenu identique.
Comme le test effectué consiste à comparer chaque Item de la ComboBox (provenant d’une ObservableCollection AllCustomers) avec l’item contenu dans le SelectedProject, le test retournera en permanence « false » puisque bien qu’ayant une partie de contenu en commun, ce ne sont pas les mêmes objets.
La solution : passer par les ID des objets. Les classes dérivants des ItemControls permettent d’intégrer pour chaque item une paire clé/valeur. Si on ne peut pas passer simplement par l’objet Item, il faut passer une de ses propriétés. Voici ce que cela donne :
Toute la différence est là : le DisplayMemberPath définit quelle propriété de l’objet Item on souhaite afficher et le SelectedValuePath définit quant à lui le chemin vers la propriété servant de clé. Il ne reste qu’à lier non pas le SelectedItem, mais le SelectedValue directement sur l’objet source (comme je le disais au-dessus, XAML permet une navigation parmi les membres d’un objet. On peut ainsi naviguer jusqu’à l’ID du Customer lié au projet).
Solution alternative
Si pour quelque raison que ce soit vous ne souhaitez pas/pouvez pas passer par les clés des items de votre ItemControl, une autre solution consiste à surcharger la méthode Equals() pour la classe en question. Dans mon cas, cela aurait donné le code suivant :
// I use partial class because my Customer class is a self-generated entity with Entity Framework.
public partial class Customer()
{
public override bool Equals(object obj)
{
if (obj == null)
return false;
if (Object.ReferenceEquals(this, obj))
return true;
if (this.GetType() != obj.GetType())
return false;
//ID comparison is the new Equals paradigm
return ((Customer)obj).ID == this.ID;
}
}
J’espère que cette astuce vous aidera à lier correctement vos ItemControls dans les 2 sens ! N’hésitez pas à réagir via les commentaires ci-dessous.






