Domanda

Quando desidero associare un controllo a una proprietà del mio oggetto, devo fornire il nome della proprietà come stringa.Questo non è molto buono perché:

  1. Se la proprietà viene rimossa o rinominata, non ricevo un avvertimento del compilatore.
  2. Se si rinomina la proprietà con uno strumento di refactoring, è probabilmente l'associazione dati non sarà aggiornato.
  3. Non ricevo un errore fino a quando runtime se il tipo della proprietà è sbagliato, ad es.associazione di un numero intero a un selettore di date.

Esiste un modello di progettazione che aggira questo problema, ma ha comunque la facilità d'uso dell'associazione dati?

(Questo è un problema in WinForm, Asp.net e WPF e molto probabilmente in molti altri sistemi)

Ora ho trovato"soluzioni alternative per l'operatore nameof() in C#:associazione dati typesafe" che rappresenta anche un buon punto di partenza per una soluzione.

Se sei disposto a utilizzare un post processore dopo aver compilato il codice, notifypropertyweaver vale la pena guardarlo.


Qualcuno conosce una buona soluzione per WPF quando i collegamenti vengono eseguiti in XML anziché in C#?

È stato utile?

Soluzione

Grazie a Oliver per avermi fatto iniziare, ora ho una soluzione che supporta il refactoring ed è sicura.Mi consente inoltre di implementare INotifyPropertyChanged in modo che gestisca la ridenominazione delle proprietà.

Il suo utilizzo è simile a:

checkBoxCanEdit.Bind(c => c.Checked, person, p => p.UserCanEdit);
textBoxName.BindEnabled(person, p => p.UserCanEdit);
checkBoxEmployed.BindEnabled(person, p => p.UserCanEdit);
trackBarAge.BindEnabled(person, p => p.UserCanEdit);

textBoxName.Bind(c => c.Text, person, d => d.Name);
checkBoxEmployed.Bind(c => c.Checked, person, d => d.Employed);
trackBarAge.Bind(c => c.Value, person, d => d.Age);

labelName.BindLabelText(person, p => p.Name);
labelEmployed.BindLabelText(person, p => p.Employed);
labelAge.BindLabelText(person, p => p.Age);

La classe person mostra come implementare INotifyPropertyChanged in modo sicuro (o vedi questa risposta per un altro modo piuttosto carino di implementare INotifyPropertyChanged, ActiveSharp - INotifyPropertyChanged automatico sembra anche bello):

public class Person : INotifyPropertyChanged
{
   private bool _employed;
   public bool Employed
   {
      get { return _employed; }
      set
      {
         _employed = value;
         OnPropertyChanged(() => c.Employed);
      }
   }

   // etc

   private void OnPropertyChanged(Expression<Func<object>> property)
   {
      if (PropertyChanged != null)
      {
         PropertyChanged(this, 
             new PropertyChangedEventArgs(BindingHelper.Name(property)));
      }
   }

   public event PropertyChangedEventHandler PropertyChanged;
}

La classe helper di associazione WinForms contiene la sostanza che fa funzionare tutto:

namespace TypeSafeBinding
{
    public static class BindingHelper
    {
        private static string GetMemberName(Expression expression)
        {
            // The nameof operator was implemented in C# 6.0 with .NET 4.6
            // and VS2015 in July 2015. 
            // The following is still valid for C# < 6.0

            switch (expression.NodeType)
            {
                case ExpressionType.MemberAccess:
                    var memberExpression = (MemberExpression) expression;
                    var supername = GetMemberName(memberExpression.Expression);
                    if (String.IsNullOrEmpty(supername)) return memberExpression.Member.Name;
                    return String.Concat(supername, '.', memberExpression.Member.Name);
                case ExpressionType.Call:
                    var callExpression = (MethodCallExpression) expression;
                    return callExpression.Method.Name;
                case ExpressionType.Convert:
                    var unaryExpression = (UnaryExpression) expression;
                    return GetMemberName(unaryExpression.Operand);
                case ExpressionType.Parameter:
                case ExpressionType.Constant: //Change
                    return String.Empty;
                default:
                    throw new ArgumentException("The expression is not a member access or method call expression");
            }
        }

        public static string Name<T, T2>(Expression<Func<T, T2>> expression)
        {
            return GetMemberName(expression.Body);
        }

        //NEW
        public static string Name<T>(Expression<Func<T>> expression)
        {
           return GetMemberName(expression.Body);
        }

        public static void Bind<TC, TD, TP>(this TC control, Expression<Func<TC, TP>> controlProperty, TD dataSource, Expression<Func<TD, TP>> dataMember) where TC : Control
        {
            control.DataBindings.Add(Name(controlProperty), dataSource, Name(dataMember));
        }

        public static void BindLabelText<T>(this Label control, T dataObject, Expression<Func<T, object>> dataMember)
        {
            // as this is way one any type of property is ok
            control.DataBindings.Add("Text", dataObject, Name(dataMember));
        }

        public static void BindEnabled<T>(this Control control, T dataObject, Expression<Func<T, bool>> dataMember)
        {       
           control.Bind(c => c.Enabled, dataObject, dataMember);
        }
    }
}

Questo sfrutta molte delle novità di C# 3.5 e mostra cosa è possibile fare.Ora, se solo avessimo avuto macro igieniche il programmatore lisp potrebbe smettere di chiamarci cittadini di seconda classe)

Altri suggerimenti

L'operatore nameof è stato implementato in C # 6.0 con .NET 4.6 e VS2015 nel luglio 2015. Quello che segue è ancora valida per C # <6.0

Per evitare le stringhe che contengono i nomi di proprietà, ho scritto una semplice classe con alberi di espressione di restituire il nome del membro:

using System;
using System.Linq.Expressions;
using System.Reflection;

public static class Member
{
    private static string GetMemberName(Expression expression)
    {
        switch (expression.NodeType)
        {
            case ExpressionType.MemberAccess:
                var memberExpression = (MemberExpression) expression;
                var supername = GetMemberName(memberExpression.Expression);

                if (String.IsNullOrEmpty(supername))
                    return memberExpression.Member.Name;

                return String.Concat(supername, '.', memberExpression.Member.Name);

            case ExpressionType.Call:
                var callExpression = (MethodCallExpression) expression;
                return callExpression.Method.Name;

            case ExpressionType.Convert:
                var unaryExpression = (UnaryExpression) expression;
                return GetMemberName(unaryExpression.Operand);

            case ExpressionType.Parameter:
                return String.Empty;

            default:
                throw new ArgumentException("The expression is not a member access or method call expression");
        }
    }

    public static string Name<T>(Expression<Func<T, object>> expression)
    {
        return GetMemberName(expression.Body);
    }

    public static string Name<T>(Expression<Action<T>> expression)
    {
        return GetMemberName(expression.Body);
    }
}

È possibile utilizzare questa classe come segue. Anche se è possibile utilizzarlo solo nel codice (quindi non in XAML), è abbastanza utile (almeno per me), ma il codice non è ancora typesafe. Si potrebbe estendere il metodo Nome con un secondo argomento tipo che definisce il valore restituito della funzione, che vincolare il tipo di proprietà.

var name = Member.Name<MyClass>(x => x.MyProperty); // name == "MyProperty"

Fino ad ora non ho trovato nulla che risolve il problema di sicurezza rispetto ai tipi di associazione dati.

Con i migliori saluti

Il Framework 4.5 ci fornisce la CallerMemberNameAttribute , che fa passare il nome della proprietà come una stringa inutile:

private string m_myProperty;
public string MyProperty
{
    get { return m_myProperty; }
    set
    {
        m_myProperty = value;
        OnPropertyChanged();
    }
}

private void OnPropertyChanged([CallerMemberName] string propertyName = "none passed")
{
    // ... do stuff here ...
}

Se si lavora su Framework 4.0 con KB2468871 installato , è possibile installare il Microsoft BCL Compatibility pack NuGet , che ha anche fornisce questo attributo.

Questo blog solleva alcune buone domande circa le prestazioni di questo approccio . Si potrebbe migliorare su tali carenze convertendo l'espressione di una stringa come parte di una sorta di inizializzazione statico.

La meccanica effettiva potrebbe essere un po 'sgradevole, ma sarebbe ancora dai tipi, e approssimativamente pari prestazioni al INotifyPropertyChanged grezzo.

Qualcosa di un po 'come questo:

public class DummyViewModel : ViewModelBase
{
    private class DummyViewModelPropertyInfo
    {
        internal readonly string Dummy;

        internal DummyViewModelPropertyInfo(DummyViewModel model)
        {
            Dummy = BindingHelper.Name(() => model.Dummy);
        }
    }

    private static DummyViewModelPropertyInfo _propertyInfo;
    private DummyViewModelPropertyInfo PropertyInfo
    {
        get { return _propertyInfo ?? (_propertyInfo = new DummyViewModelPropertyInfo(this)); }
    }

    private string _dummyProperty;
    public string Dummy
    {
        get
        {
            return this._dummyProperty;
        }
        set
        {
            this._dummyProperty = value;
            OnPropertyChanged(PropertyInfo.Dummy);
        }
    }
}

Un modo per ottenere feedback se i collegamenti sono interrotti è creare un DataTemplate e dichiarare il suo DataType come il tipo del ViewModel a cui si associa, ad es.se hai un PersonView e un PersonViewModel dovresti fare quanto segue:

  1. Dichiarare un DataTemplate con DataType = PersonViewModel e una chiave (ad es.Modello Persona)

  2. Taglia tutto il file xaml di PersonView e incollalo nel modello di dati (che idealmente può trovarsi proprio nella parte superiore del file PersonView.

3a.Crea un ContentControl e imposta ContentTemplate = PersonTemplate e associa il relativo contenuto a PersonViewModel.

3b.Un'altra opzione è non fornire una chiave a DataTemplate e non impostare ContentTemplate di ContentControl.In questo caso WPF capirà quale DataTemplate utilizzare, poiché sa a quale tipo di oggetto ti stai collegando.Cercherà nell'albero e troverà il tuo DataTemplate e poiché corrisponde al tipo di associazione, lo applicherà automaticamente come ContentTemplate.

Ti ritroverai essenzialmente con la stessa visualizzazione di prima, ma poiché hai mappato DataTemplate su un DataType sottostante, strumenti come Resharper possono darti feedback (tramite identificatori di colore - Resharper-Opzioni-Impostazioni-Identificatori di colore) per verificare se i tuoi collegamenti sono interrotti o no.

Non riceverai comunque avvisi del compilatore, ma potrai verificare visivamente la presenza di associazioni interrotte, il che è meglio che dover controllare avanti e indietro tra la tua vista e il viewmodel.

Un altro vantaggio di queste informazioni aggiuntive fornite è che possono essere utilizzate anche per rinominare i refactoring.Per quanto ricordo, Resharper è in grado di rinominare automaticamente i collegamenti sui DataTemplate digitati quando il nome della proprietà ViewModel sottostante viene modificato e viceversa.

  

1. Se la proprietà viene rimosso o rinominato, non ho ricevuto un avviso del compilatore.

     

2. Se una ridenominazione della proprietà con uno strumento di refactoring, è probabile che i dati non vincolante verrà aggiornato.

     

3.I non si ottiene un errore fino al runtime se il tipo della proprietà è sbagliato, per esempio associare un numero intero in una data selettore.

Sì, Ian, che sono esattamente i problemi con l'associazione di dati nome-string guidati. È richiesto un design-pattern. Ho progettato il modello Type-Safe View Model (TVM), che è una concrezione della parte View Modello del pattern Model-View-ViewModel (MVVM). Si basa su un tipo di sicurezza di legame, simile alla tua propria risposta. Ho appena postato una soluzione per WPF:

http: // www.codeproject.com/Articles/450688/Enhanced-MVVM-Design-w-Type-Safe-View-Models-TVM

x: bind (chiamato anche "associazioni di dati compilati") per XAML (universal app) in Windows 10 e Windows Phone 10 può risolvere questo problema, vedere https://channel9.msdn.com/Events/Build/2015/3-635

Non riesco a trovare la documentazione sulla linea per esso, ma non hanno messo molto impegno in, in quanto si tratta di qualcosa che non verrà utilizzato per un certo tempo. Tuttavia questa risposta dovrebbe essere un puntatore utile ad altre persone.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top