Juillet 2016

Volume 31, numéro 7

Cet article a fait l'objet d'une traduction automatique.

Liaison de données - Une meilleure façon d’intégrer la liaison de données à .NET

Par Mark Sowul

Liaison de données est une technique puissante pour le développement d’interfaces utilisateur : Elle permet de simplifier la séparation entre la logique d’affichage et la logique commerciale, ainsi que le test du code obtenu. Bien que présent dans Microsoft .NET Framework depuis le début, la liaison de données est devenu plus importante avec l’arrivée de Windows Presentation Foundation (WPF) et XAML, comme il constitue la « colle » entre la vue et le ViewModel dans le modèle Model-View-ViewModel (MVVM).

L’inconvénient de l’implémentation de liaison de données a toujours été la nécessité de chaînes magiques et de code réutilisable, pour diffuser les modifications apportées aux propriétés et à lier les éléments d’interface utilisateur. Au fil des années, divers outils et techniques ont contribué à réduire le problème ; Cet article vise à simplifier davantage le processus.

Tout d’abord, je vais examiner les bases de l’implémentation de liaison de données, ainsi que des techniques courantes pour simplifier (si vous êtes déjà familiarisé avec l’objet, vous pouvez ignorer ces sections). Après cela, je développerai une technique que vous ne pouvez pas avoir considéré (« une troisième façon ») et présente des solutions pour la conception connexes difficultés lors du développement d’applications à l’aide de MVVM. Vous pouvez obtenir la version terminée de l’infrastructure de que développer ici dans le téléchargement de code qui accompagne cet article, ou ajouter le package NuGet de SolSoft.DataBinding pour vos propres projets.

Notions de base : INotifyPropertyChanged

L’implémentation d’INotifyPropertyChanged est la meilleure façon de permettre à un objet d’être lié à une interface utilisateur. Il est assez simple contenant qu’un seul membre : l’événement PropertyChanged. L’objet doit déclencher cet événement lorsqu’une propriété change, afin d’informer la vue qu’il doit être actualisé sa représentation sous forme de valeur de la propriété.

L’interface est simple, mais qui n’est pas. Déclencher manuellement l’événement avec des noms de propriété de texte codé en dur de n’est pas une solution qui s’adapte bien ni il résister à la refactorisation : Vous devez veiller à s’assurer que le nom textuel reste synchronisé avec le nom de propriété dans le code. Cela ne sera pas rendre votre collaboration précieuse à vos successeurs. Voici un exemple :

public int UnreadItemCount
{
  get
  {
    return m_unreadItemCount;
  }
  set
  {
    m_unreadItemCount = value;
    OnNotifyPropertyChanged(
      new PropertyChangedEventArgs("UnreadItemCount")); // Yuck
  }
}

Il existe plusieurs personnes techniques ont développé en réponse, afin de maintenir leur validité (voir, par exemple, la question de débordement de pile à bit.ly/24ZQ7CY) ; la plupart d'entre elles appartiennent à un des deux types.

Technique courante 1 : Base Class

Permet de simplifier la situation est avec une classe de base afin de réutiliser une partie de la logique de code réutilisable. Il fournit également plusieurs façons d’obtenir le nom de propriété par programme, au lieu de coder en dur il.

L’obtention du nom de propriété avec des Expressions : Le .NET Framework 3.5 a introduit les expressions, permettant à l’inspection de runtime de la structure du code. LINQ utilise cette API grand effet, par exemple, pour traduire des requêtes LINQ de .NET dans les instructions SQL. Les développeurs entreprenante ont également utilisé cette API pour examiner les noms de propriété. À l’aide d’une classe de base pour effectuer cette inspection, la méthode setter précédente pourrait être réécrit en tant que :

public int UnreadItemCount
...
set
{
  m_unreadItemCount = value;
  RaiseNotifyPropertyChanged(() => UnreadItemCount);
}

De cette façon, renommer UnreadItemCount sera également renommer la référence d’expression, donc le code fonctionnera toujours. La signature de RaiseNotifyPropertyChanged se présente comme suit :

void RaiseNotifyPropertyChanged<T>(Expression<Func<T>> memberExpression)

Il existe diverses techniques permettant de récupérer le nom de propriété de la memberExpression. Le blog de c# MSDN à l’adresse bit.ly/25baMHM fournit un exemple simple :

public static string GetName<T>(Expression<Func<T>> e)
{
  var member = (MemberExpression)e.Body;
  return member.Member.Name;
}

StackOverflow présente une liste plus complète à bit.ly/23Xczu2. Dans tous les cas, il existe un inconvénient de cette technique : Extraction du nom de l’expression utilise la réflexion et la réflexion est lente. La surcharge des performances peut être significative, selon le sont des notifications de modification de propriété de nombre.

L’obtention du nom de propriété avec CallerMemberName : 5.0 c# et le .NET Framework 4.5 remise supplémentaire permet d’extraire le nom de propriété à l’aide de l’attribut CallerMemberName (vous pouvez utiliser ceci avec les versions antérieures du .NET Framework via le package NuGet de Microsoft.Bcl). Cette fois, le compilateur fait tout le travail, il n’existe aucune surcharge d’exécution. Avec cette approche, la méthode devient :

void RaiseNotifyPropertyChanged<T>([CallerMemberName] string propertyName = "")
And the call to it is:
public int UnreadItemCount
...
set
{
  m_unreadItemCount = value;
  RaiseNotifyPropertyChanged();
}

L’attribut indique au compilateur pour renseigner le nom de l’appelant, UnreadItemCount, comme la valeur du paramètre facultatif propertyName.

L’obtention du nom de propriété avec le nom : L’attribut CallerMemberName était probablement conçu spécialement pour ce cas de figure (déclenchement PropertyChanged dans une classe de base), mais dans c# 6, l’équipe du compilateur enfin fourni quelque chose de beaucoup plus largement utiles : le mot clé de nom. Nom est utile à de nombreuses fins ; Dans ce cas, si le code basé sur des expressions remplacer par nom, une fois encore le compilateur ne tout le travail (aucune surcharge d’exécution). Il est important de noter que ce est strictement une fonctionnalité de la version du compilateur et non une fonctionnalité de la version .NET : Vous pouvez utiliser cette technique et toujours cible .NET Framework 2.0. Toutefois, vous (et tous les membres de votre équipe) doivent utiliser au moins Visual Studio 2015. À l’aide du nom ressemble à ceci :

public int UnreadItemCount
...
set
{
  m_unreadItemCount = value;
  RaiseNotifyPropertyChanged(nameof(UnreadItemCount));
}

Il existe un problème de général, cependant, avec les techniques de la classe de base : Il « augmente votre classe de base, « comme on accède. Si vous souhaitez que votre modèle de vue pour étendre une classe différente, vous êtes perdu. Il effectue également rien pour gérer des propriétés « dépendantes » (par exemple, une propriété FullName qui concatène FirstName et LastName : Toute modification apportée à FirstName et LastName doit également déclencher d’une modification sur FullName).

Technique courante 2 : La programmation orientée aspect

Programmation orientée aspect (AOP) est une technique qui en fait « post-traite » votre code compilé, au moment de l’exécution ou avec une étape postérieure à la compilation, afin d’ajouter certains comportements (appelés un « aspect »). En règle générale, l’objectif est de remplacer les répétitions du code, telles que la journalisation ou la gestion des exceptions (ce que l'on appelle « problèmes transversaux »). Sans surprise, implémenter INotifyPropertyChanged est un bon candidat.

Il existe plusieurs toolkits disponibles pour cette approche. PostSharp est un (bit.ly/1Xmq4n2). J’ai été agréablement surpris d’apprendre qu’il gère correctement les propriétés dépendantes (par exemple, la propriété FullName décrite précédemment). Une infrastructure open source appelée « Fody » est similaire (bit.ly/1wXR2VA).

Il s’agit d’une approche intéressante ; ses inconvénients peut ne pas être significatifs. Certaines implémentations interceptent le comportement au moment de l’exécution, ce qui entraîne une baisse des performances. Les infrastructures postérieure à la compilation, en revanche, ne doit pas poser toute surcharge d’exécution, mais peuvent nécessiter une sorte d’installation ou de configuration. PostSharp actuellement proposé comme une extension de Visual Studio. Son édition « Express » gratuite limite l’utilisation de l’aspect INotifyPropertyChanged aux classes de 10, cela signifie probablement un coût monétaire. Fody, est en revanche, un package NuGet gratuit, ce qui rend semble être un choix incontestable. Malgré tout, considérez qu’avec n’importe quelle infrastructure de Poa le code que vous écrivez n’est pas identique au code en cours d’exécution... et vous le débogage.

Une troisième façon

Une autre façon de gérer cela consiste à exploiter la conception orientée objet : Avoir les propriétés elles-mêmes être chargé de déclencher les événements ! Il n’est pas une idée révolutionnaire en particulier, mais ce n’était pas que j’ai rencontrés en dehors de mes propres projets. Dans sa forme la plus basique, il peut se présenter comme suit :

public class NotifyProperty<T>
{
  public NotifyProperty(INotifyPropertyChanged owner, string name, T initialValue);
  public string Name { get; }
  public T Value { get; }
  public void SetValue(T newValue);
}

L’idée est de fournir la propriété avec son nom et une référence à son propriétaire, puis de laisser le travail de déclencher l’événement PropertyChanged : quelque chose comme :

public void SetValue(T newValue)
{
  if(newValue != m_value)
  {
    m_value = newValue;
    m_owner.PropertyChanged(m_owner, new PropertyChangedEventArgs(Name));
  }
}

Le problème est que cela ne fonctionnera pas réellement : Je ne peux pas déclencher un événement à partir d’une autre classe similaire. J’ai besoin d’un type de contrat avec la classe propriétaire pour déclencher son événement PropertyChanged : c’est exactement le travail d’une interface, donc je vais créer un :

public interface IRaisePropertyChanged
{
  void RaisePropertyChanged(string propertyName)
}

Une fois que j’ai cette interface, je peux implémenter réellement NotifyProperty.SetValue :

public void SetValue(T newValue)
{
  if(newValue != m_value)
  {
    m_value = newValue;
    m_owner.RaisePropertyChanged(this.Name);
  }
}

Mise en œuvre IRaisePropertyChanged : Exiger le propriétaire de la propriété implémenter une interface signifie que chaque classe de modèle de vue nécessite certaines réutilisable, comme illustré dans Figure 1. La première partie est requise pour toute classe implémenter INotifyPropertyChanged ; la deuxième partie est spécifique à la nouvelle IRaisePropertyChanged. Notez que, étant donné que la méthode RaisePropertyChanged n’est pas conçue pour une utilisation générale, je préfère implémenter explicitement.

Figure 1 Code requis pour implémenter IRaisePropertyChanged

// PART 1: required for any class that implements INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
{
  // In C# 6, you can use PropertyChanged?.Invoke.
  // Otherwise I'd suggest an extension method.
  var toRaise = PropertyChanged;
  if (toRaise != null)
    toRaise(this, args);
}
// PART 2: IRaisePropertyChanged-specific
protected virtual void RaisePropertyChanged(string propertyName)
{
  OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
// This method is only really for the sake of the interface,
// not for general usage, so I implement it explicitly.
void IRaisePropertyChanged.RaisePropertyChanged(string propertyName)
{
  this.RaisePropertyChanged(propertyName);
}

Je pourrais placer ce code réutilisable dans une classe de base et l’étendre, ce qui va me mettre à mon argumentation antérieures. Après tout, si j’applique CallerMemberName à la méthode RaisePropertyChanged, j’ai réinventé essentiellement la première technique, par conséquent, quel est l’intérêt ? Dans les deux cas, je pourrais simplement copier le code réutilisable à d’autres classes si elles ne peut pas dériver une classe de base.

Une différence essentielle par rapport à la technique de la classe de base antérieure n’est dans ce cas aucune logique réelle dans le code ; toute la logique est encapsulée dans la classe NotifyProperty. Vérifie si la valeur de propriété a été modifiée avant de déclencher l’événement est une logique simple, mais il est toujours préférable de ne pas dupliquer. Considérez ce qui se passerait si vous souhaitez utiliser un autre IEqualityComparer pour effectuer la vérification. Avec ce modèle, vous devez modifier uniquement la classe NotifyProperty. Même si vous avez plusieurs classes avec le même réutilisable IRaisePropertyChanged, chaque implémentation pourrait bénéficier les modifications apportées à NotifyProperty sans avoir à modifier le code lui-même. Indépendamment des modifications de comportement que vous souhaiterez introduire, le code IRaisePropertyChanged est très peu susceptible de changer.

Assemblage des morceaux : J’ai maintenant l’interface que doit implémenter le modèle de vue et la classe NotifyProperty utilisé pour les propriétés qui seront liée aux données. La dernière étape consiste à construire le NotifyProperty ; Pour ce faire, vous devez toujours passer dans un nom de propriété, d’une certaine manière. Si vous avez la chance être à l’aide de C# 6, ceci se fait facilement avec l’opérateur de nom. Si non, vous pouvez créer à la place la NotifyProperty à l’aide d’expressions, comme à l’aide d’une méthode d’extension (Malheureusement, il n’existe nulle part pour CallerMemberName aider à cette heure) :

public static NotifyProperty<T> CreateNotifyProperty<T>(
  this IRaisePropertyChanged owner,
  Expression<Func<T>> nameExpression, T initialValue)
{
  return new NotifyProperty<T>(owner,
    ObjectNamingExtensions.GetName(nameExpression),
    initialValue);
}
// Listing of GetName provided earlier

Avec cette approche, vous payez toujours une réflexion de coût, mais uniquement lors de la création d’un objet, plutôt que chaque fois qu’une propriété change. Si c’est toujours trop coûteuse (que vous créez plusieurs objets), vous pouvez toujours mettre en cache un appel à GetName et gardez cela comme une valeur statique en lecture seule dans la classe de modèle de vue. Dans les deux cas, Figure 2 montre un exemple d’un modèle de vue simple.

Figure 2 de base ViewModel avec un NotifyProperty

public class LogInViewModel : IRaisePropertyChanged
{
  public LogInViewModel()
  {
    // C# 6
    this.m_userNameProperty = new NotifyProperty<string>(
      this, nameof(UserName), null);
    // Extension method using expressions
    this.m_userNameProperty = this.CreateNotifyProperty(() => UserName, null);
  }
  private readonly NotifyProperty<string> m_userNameProperty;
  public string UserName
  {
    get
    {
      return m_userNameProperty.Value;
    }
    set
    {
      m_userNameProperty.SetValue(value);
    }
  }
  // Plus the IRaisePropertyChanged code in Figure 1 (otherwise, use a base class)
}

Liaison et je parle de noms de changement de nom : en cours, il est un bon moment pour aborder un autre problème de liaison de données. En toute sécurité qui déclenche l’événement PropertyChanged sans une chaîne codée en dur est le début de résister à la refactorisation ; la liaison elle-même de données seront l’autre moitié. Si vous renommez une propriété qui est utilisée pour la liaison en XAML, réussite, dois-je dire, peut (voir, par exemple, bit.ly/1WCWE5m).

L’alternative consiste à coder les liaisons de données manuellement dans le fichier code-behind. Par exemple :

// Constructor
public LogInDialog()
{
  InitializeComponent();
  LogInViewModel forNaming = null;
  m_textBoxUserName.SetBinding(TextBox.TextProperty,
    ObjectNamingExtensions.GetName(() => forNaming.UserName);
  // Or with C# 6, just nameof(LogInViewModel.UserName)
}

Il est un peu bizarre que cet objet uniquement pour tirer parti de la fonctionnalité d’expressions null, mais il ne fonctionne pas (vous ne sont pas nécessaires si vous avez accès au nom).

Cette technique trouver utiles, mais je ne reconnaissent pas les compromis. De plus, si je renomme la propriété de nom d’utilisateur, je peux être certain que la refactorisation fonctionnera. Un autre avantage significatif est que « Rechercher toutes les références » fonctionne comme prévu.

Côté moins, il n’est pas nécessairement aussi simple et naturelle en effectuant la liaison en XAML, et il m’empêche de conserver la conception de l’interface utilisateur « indépendant ». Je ne peux pas simplement reconcevoir l’apparence de l’outil de fusion sans modifier le code, par exemple. En outre, cette technique ne fonctionne pas sur les modèles de données ; Vous pouvez extraire ce modèle dans un contrôle personnalisé, mais ce n’est plus d’efforts.

Au total, on y gagne flexibilité pour modifier la partie « modèle de données », au détriment de la souplesse du côté « vue ». En général, c’est à vous indique si les avantages justifient déclarant les liaisons de cette manière.

Propriétés « Dérivées »

Précédemment, j’ai décrit un scénario dans lequel il est particulièrement gênant de déclencher l’événement PropertyChanged, à savoir pour les propriétés dont la valeur dépend d’autres propriétés. J’ai mentionné l’exemple simple d’une propriété FullName qui dépend de FirstName et LastName. Mon objectif pour implémenter ce scénario est de ces objets NotifyProperty base (FirstName et LastName), ainsi que la fonction pour calculer la valeur dérivée à partir de ceux-ci (par exemple, FirstName.Value + » « + LastName.Value) et qui, de produire un objet qui gère automatiquement le reste pour moi. Pour ce faire, il existe quelques ajustements, que je vais en faire à mon NotifyProperty d’origine.

La première tâche consiste à exposer un événement ValueChanged distinct sur NotifyProperty. La propriété dérivée écoute de cet événement dans ses propriétés sous-jacentes et répondre en calculant une nouvelle valeur (et déclencher l’événement PropertyChanged approprié pour ce dernier). La deuxième tâche consiste à extraire une interface, IProperty < T >, pour encapsuler les fonctionnalités NotifyProperty. Entre autres choses, cela me permet de possèdent des propriétés dérivés proviennent d’autres dérivés des propriétés. L’interface qui en résulte est simple et est répertoriée ici (les modifications correspondantes aux NotifyProperty sont très simples, donc je ne les répertorier) :

public interface IProperty<TValue>
{
  string Name { get; }
  event EventHandler<ValueChangedEventArgs> ValueChanged;
  TValue Value { get; }
}

Création de la classe DerivedNotifyProperty semble simple, aussi, avant le démarrage de la tentative d’assemblage des éléments. L’idée était de tirer dans les propriétés sous-jacentes et une fonction pour calculer une valeur nouvelle d’eux, mais qui immédiatement exécute des problèmes en raison des génériques. Il n’existe aucun moyen pratique de prendre dans plusieurs types de propriété :

// Attempted constructor
public DerivedNotifyProperty(IRaisePropertyChanged owner,
  string propertyName, IProperty<T1> property1, IProperty<T2> property2,
  Func<T1, T2, TDerived> derivedValueFunction)

Je peux contourner la première moitié du problème (en acceptant plusieurs types génériques) à l’aide de créer des méthodes statiques au lieu de cela :

static DerivedNotifyProperty<TDerived> CreateDerivedNotifyProperty
  <T1, T2, TDerived>(this IRaisePropertyChanged owner,
  string propertyName, IProperty<T1> property1, IProperty<T2> property2,
  Func<T1, T2, TDerived> derivedValueFunction)

Mais la propriété dérivée doit toujours écouter l’événement ValueChanged de chaque propriété de base. Cette résolution nécessite deux étapes. Tout d’abord, je vais extraire l’événement ValueChanged dans une interface distincte :

public interface INotifyValueChanged // No generic type!
{
  event EventHandler<ValueChangedEventArgs> ValueChanged;
}
public interface IProperty<TValue> : INotifyValueChanged
{
  string Name { get; }
  TValue Value { get; }
}

Cela permet le DerivedNotifyProperty à prendre dans le INotifyValueChanged non générique, au lieu du IProperty générique < T >. Ensuite, je dois calculer la nouvelle valeur sans les génériques : Je prennent la derivedValueFunction d’origine qui accepte deux paramètres génériques et de celui, créez une fonction anonyme qui ne requiert aucun paramètre, au lieu de cela, elle fait référence les valeurs de deux propriétés transmises dans. En d’autres termes, je vais créer une fermeture. Vous pouvez voir ce processus dans le code suivant :

static DerivedNotifyProperty<TDerived> CreateDerivedNotifyProperty
  <T1, T2, TDerived>(this IRaisePropertyChanged owner,
  string propertyName, IProperty<T1> property1, IProperty<T2> property2,
  Func<T1, T2, TDerived> derivedValueFunction)
{
  // Closure
  Func<TDerived> newDerivedValueFunction =
    () => derivedValueFunction (property1.Value, property2.Value);
  return new DerivedNotifyProperty<TValue>(owner, propertyName,
    newDerivedValueFunction, property1, property2);
}

La fonction nouvelle « valeur dérivé » est simplement Func < TDerived > sans paramètres ; maintenant le DerivedNotifyProperty ne nécessite aucune connaissance des types de propriété sous-jacent, donc je peux créer correctement un à partir de plusieurs propriétés de types différents.

L’autres subtilité est quand appeler réellement dérivée fonction de valeur. Une implémentation évidente serait pour écouter l’événement ValueChanged de chaque propriété sous-jacent et appeler la fonction chaque fois qu’une propriété est modifiée, mais qui est inefficace lorsque plusieurs propriétés sous-jacentes changent dans la même opération (par exemple un bouton « Réinitialiser » qui efface un formulaire). Une meilleure idée consiste à fournir la valeur à la demande (et mettre en cache) et la rend si les propriétés sous-jacentes changent. Lazy < T > est une façon idéale de cette implémentation.

Vous pouvez voir une liste abrégée de la classe DerivedNotifyProperty dans Figure 3. Notez que la classe accepte un nombre arbitraire de propriétés à écouter, bien que la liste seulement la méthode de création de deux propriétés sous-jacentes, créer des surcharges supplémentaires qui prennent dans une propriété sous-jacente, trois propriétés sous-jacentes et ainsi de suite.

Figure 3 Core implémentation de DerivedNotifyProperty

public class DerivedNotifyProperty<TValue> : IProperty<TValue>
{
  private readonly IRaisePropertyChanged m_owner;
  private readonly Func<TValue> m_getValueProperty;
  public DerivedNotifyProperty(IRaisePropertyChanged owner,
    string derivedPropertyName, Func<TValue> getDerivedPropertyValue,
    params INotifyValueChanged[] valueChangesToListenFor)
  {
    this.m_owner = owner;
    this.Name = derivedPropertyName;
    this.m_getValueProperty = getDerivedPropertyValue;
    this.m_value = new Lazy<TValue>(m_getValueProperty);
    foreach (INotifyValueChanged valueChangeToListenFor in valueChangesToListenFor)
      valueChangeToListenFor.ValueChanged += (sender, e) => RefreshProperty();
  }
  // Name property and ValueChanged event omitted for brevity 
  private Lazy<TValue> m_value;
  public TValue Value
  {
    get
    {
      return m_value.Value;
    }
  }
  public void RefreshProperty()
  {
    // Ensure we retrieve the value anew the next time it is requested
    this.m_value = new Lazy<TValue>(m_getValueProperty);
    OnValueChanged(new ValueChangedEventArgs());
    m_owner.RaisePropertyChanged(Name);
  }
}

Notez que les propriétés sous-jacentes peut provenir des propriétaires différents. Par exemple, supposons que vous disposez d’un modèle de vue adresse avec une propriété IsAddressValid. Vous avez également un modèle de vue de commande qui contient deux modèles d’affichage adresse pour les adresses de facturation et de livraison. Il serait judicieux de créer une propriété IsOrderValid sur le modèle d’affichage de commande parent qui combine les propriétés de IsAddressValid des modèles d’affichage d’adresse enfant, permet d’envoyer la commande uniquement si les deux adresses sont valides. Pour ce faire, le modèle de vue adresse expose les deux bool IsAddressValid {get ;} et IProperty < bool > IsAddressValidProperty {get ;}, le modèle d’affichage de commande permettant de créer un DerivedNotifyProperty qui référence des objets enfant IsAddressValidProperty.

L’utilité de DerivedNotifyProperty

L’exemple FullName que j’ai donné une propriété dérivée est assez artificiels, mais je ne veux pas discuter de certains cas de l’utilisation réelle et les lient aux quelques principes de conception. J’ai abordé juste un exemple : IsValid. Il s’agit d’un moyen relativement simple et puissant pour désactiver le bouton « Enregistrer » sur un formulaire, par exemple. Notez qu’il n’y a rien qui vous oblige à utiliser cette technique que dans le contexte d’un modèle d’affichage de l’interface utilisateur. Vous pouvez l’utiliser pour valider des objets métier, trop ; Il suffit d’implémenter IRaisePropertyChanged.

Il est une deuxième situation où les propriétés dérivées sont extrêmement utiles dans les scénarios de « zoom avant ». À titre d’exemple, considérez une zone de liste déroulante pour sélectionner un pays, où la sélection d’un pays remplit une liste de villes. Vous avez SelectedCountry un NotifyProperty et, selon une méthode de GetCitiesForCountry, de créer des AvailableCities sous la forme d’un DerivedNotifyProperty qui sera automatiquement synchronisée lorsque le pays sélectionné est modifié.

Un troisième domaine dans lequel j’ai utilisé NotifyProperty objets est d’indiquer si un objet est « occupé. » Tandis que l’objet est considéré comme occupé, certaines fonctionnalités de l’interface utilisateur doivent être désactivées, et peut-être l’utilisateur doit voir un indicateur de progression. Il s’agit d’un scénario simple en apparence, mais il y a beaucoup de subtilité ici examinez.

La première partie est suivi si l’objet est occupé ; dans le cas le plus simple, je peux le faire avec un NotifyProperty de type Boolean. Toutefois, ce qui se passe souvent est qu’un objet peut être « occupé » pour plusieurs raisons : supposons que je charge plusieurs zones de données, éventuellement en parallèle. L’état global « occupé » dépend de si un de ces éléments sont toujours en cours. Cela paraît presque comme un travail pour les propriétés dérivées, mais il serait maladroites (voire impossible) : Je devrais une propriété pour chaque opération possible déterminer s’il est en cours. Au lieu de cela, je veux faire quelque chose comme ce qui suit pour chaque opération, à l’aide d’une seule propriété IsBusy :

try
{
  IsBusy.SetValue(true);
  await LongRunningOperation();
}
finally
{
  IsBusy.SetValue(false);
}

Pour ce faire, je crée une classe IsBusyNotifyProperty qui étend NotifyProperty < bool > et, conserver une « occupé "count. Remplacer DéfinirValeur telles que ce nombre augmente de SetValue(true), et SetValue(false) il diminue. Lorsque le compte passe de 0 à 1, puis j’appelle base. SetValue(true) et lorsqu’il passe de 1 à 0, de base. SetValue(false). De cette façon, démarrage de plusieurs résultats d’opérations en attente dans IsBusy devient true qu'uniquement une fois et par la suite, il prend la valeur false à nouveau uniquement lorsqu’ils ont fini de tout. Vous pouvez voir l’implémentation dans le téléchargement de code.

Qui s’occupe de la partie « occupée » de choses : Je peux lier « l’est indisponible » pour la visibilité d’un indicateur de progression. Toutefois, pour désactiver l’interface utilisateur, j’ai besoin le contraire. Lorsque « est indisponible » a la valeur true, « UI activée » doit être false.

XAML possède le concept d’un IValueConverter, qui convertit une valeur de (ou à partir de) une représentation de l’affichage. BooleanToVisibilityConverter en est un exemple très répandue, en XAML, « Visibilité » un élément n’est pas décrit par une valeur booléenne, mais plutôt une valeur enum. Cela signifie qu’il n’est pas possible de lier la visibilité d’un élément directement à une propriété booléenne (comme IsBusy) ; Vous devez également utiliser un convertisseur et de lier la valeur. Par exemple :

<StackPanel Visibility="{Binding IsBusy,
  Converter={StaticResource BooleanToVisibilityConverter}}" />

J’ai indiqué que « activer l’interface utilisateur » est le contraire de « est occupé » ; Il peut être tentant de créer un convertisseur de valeur pour inverser une propriété booléenne et qui permet de faire le travail :

<Grid IsEnabled="{Binding IsBusy,
   Converter={StaticResource BooleanToInverseConverter}}" />

En effet, avant que j’ai créé une classe DerivedNotifyProperty, qui est le moyen le plus simple. Il a été très fastidieux à créer une propriété distincte et le connecter à être l’inverse de IsBusy déclencher l’événement PropertyChanged approprié. Maintenant, toutefois, il est facile et sans cette barrière artificielle (c'est-à-dire paresse) faire une meilleure idée d’où il est logique d’utiliser IValueConverter.

Finalement, la vue, cependant elle peut être implémentée (WPF ou Windows Forms, par exemple ; ou même d’une application de console est un type d’affichage) : doit être une visualisation (ou « projection ») de ce qui se passe dans l’application sous-jacente, et être tenu responsable pour déterminer le mécanisme et les règles d’entreprise pour ce qui se passe. Dans ce cas, le fait que IsBusy et IsEnabled se trouvent être liées si étroitement est un détail d’implémentation ; Il n’est pas inhérente que la désactivation de l’interface utilisateur se rapporte spécifiquement si l’application est occupée.

Tel quel, je la considèrent comme une zone grise et ne prétendent avec vous si vous ne souhaitez pas utiliser un convertisseur de valeur à mettre en œuvre ce. Cependant, je peux faire beaucoup plus puissant cas en ajoutant une autre partie de l’exemple. Nous supposons que si elle perd l’accès de réseau, l’application doit également désactiver l’interface utilisateur (et afficher un panneau indiquant la situation). Ainsi, ce qui rend les trois situations : Si l’application est occupée, je dois désactiver l’interface utilisateur (et afficher une progression). Si l’application perd l’accès au réseau, je dois également désactiver l’interface utilisateur (et afficher un panneau « connexion perdue »). La situation de tiers est lorsque l’application est connectée et pas occupé et, ainsi, prêt à accepter l’entrée.

Lors de cette implémentation sans une propriété IsEnabled distincte est difficile au mieux ; Vous pouvez utiliser un MultiBinding, mais qui est toujours ungainly et non pris en charge dans tous les environnements. En fin de compte, type d’awkwardness signifie en général il existe une meilleure méthode, et nous savons maintenant il y est : cette logique est mieux gérée à l’intérieur du modèle de la vue. Il est désormais facile d’exposer les deux NotifyProperties, IsBusy et IsDisconnected et puis créer un DerivedNotifyProperty, IsEnabled, qui a la valeur true uniquement si les deux ont la valeur false.

Si vous est passé de l’itinéraire IValueConverter et état de l’interface de l’utilisateur activé directement liés à IsBusy (avec un convertisseur pour inverser il), vous auriez tout à fait un peu de travail maintenant. Si vous avez exposée à la place une propriété IsEnabled séparée, dérivée, ajouter ce nouveau bit de logique est beaucoup moins de travail, et la liaison elle-même de IsEnabled est même inutile de modifier. C’est un bon signe que vous faites des choses correctement.

Synthèse

Disposition de cette infrastructure a été un peu d’un voyage, mais la récompense est que je peux implémenter les notifications de modification de propriété sans les répétitions sans chaînes magiques et avec prise en charge de la refactorisation. Mes modèles de vue ne nécessitent pas la logique d’une classe de base particulière. Je peux créer des propriétés dérivées de déclencher également les notifications de modification appropriée sans trop d’efforts supplémentaire. Enfin, que le code est le code qui est en cours d’exécution. Et tout cela en développant une structure relativement simple avec un design orienté objet. J’espère que vous trouverez utile dans vos propres projets.


Mark Sowul est un développeur .NET consacré depuis le début et partage son large éventail d’expertise architecture et performances dans le Microsoft.NET Framework et SQL Server via son New York consulting business, Solutions de SolSoft.  Contacter à l’adresse mark@solsoftsolutions.com. Si vous trouvez ses idées fascinante et souhaitez s’abonner à son bulletin d’informations, vous abonner à eepurl.com/_K7YD.

Remercie les experts techniques suivants d'avoir relu cet article : Francis Cheung (Microsoft) et Charles Malm (Zebra Technologies)
Francis Cheung est un développeur sénior pour le Microsoft Patterns & Practices groupe. Francis a été impliqué dans un ensemble varié de projets, y compris de prisme. Il se concentre actuellement sur Azure commentaires correspondants.

Charles Malm est un jeu, .NET et ingénieur de logiciel Web et cofondateur de RealmSource, LLC.