web-dev-qa-db-ja.com

データバインディングのタイプを安全にしてリファクタリングをサポートする方法

コントロールをオブジェクトのプロパティにバインドする場合、プロパティの名前を文字列として指定する必要があります。次の理由により、これはあまり良くありません。

  1. プロパティが削除されたり名前が変更されたりしても、コンパイラの警告は表示されません。
  2. リファクタリングツールを使用してプロパティの名前を変更すると、データバインディングが更新されない可能性があります。
  3. プロパティのタイプが間違っている場合、実行時までエラーは発生しません。整数を日付チューザにバインドします。

これを回避するデザインパターンはありますが、それでもデータバインディングの使いやすさはありますか?

(これは、WinForm、Asp.net、およびWPFの問題であり、他の多くのシステムで発生する可能性があります)

" C#のnameof()演算子の回避策:タイプセーフデータバインディング "も見つかりました。これもソリューションの開始点として適しています。

コードのコンパイル後にポストプロセッサを使用する場合は、 notifypropertyweaver を検討する価値があります。


バインディングがC#ではなくXMLで行われる場合、WPFの優れたソリューションを知っている人はいますか?

69
Ian Ringrose

始めてくれたOliverに感謝します。リファクタリングをサポートし、タイプセーフなソリューションを手に入れました。また、INotifyPropertyChangedを実装して、名前が変更されるプロパティに対応できるようにします。

使い方は次のようになります。

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);

Personクラスは、タイプセーフな方法でINotifyPropertyChangedを実装する方法を示しています(または この回答を参照 INotifyPropertyChangedを実装する他のかなり良い方法については ActiveSharp-自動INotifyPropertyChanged も見栄えが良い) :

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;
}

WinFormsバインディングヘルパークラスには、すべてを機能させる重要な部分があります。

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);
        }
    }
}

これは、C#3.5の多くの新しい機能を利用し、何が可能かを示しています。 衛生的なマクロ があった場合に限り、LISPプログラマーは私たちを二級市民と呼ぶのをやめるかもしれません)

51
Ian Ringrose

Nameofオペレーターは、2015年7月に.NET 4.6とVS2015を備えたC#6.0で実装されました。以下は、C#<6.0でも引き続き有効です。

プロパティ名を含む文字列を回避するために、式ツリーを使用してメンバーの名前を返す簡単なクラスを作成しました。

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);
    }
}

このクラスは次のように使用できます。 (XAMLではなく)コードでのみ使用できますが、(少なくとも私にとっては)非常に役立ちますが、コードはまだ型保証されていません。プロパティのタイプを制約する関数の戻り値を定義する2番目のタイプ引数でメソッドNameを拡張できます。

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

これまで、データバインディングのタイプセーフの問題を解決するものは何も見つかりませんでした。

宜しくお願いします

27
Oliver Hanappi

Framework 4.5は CallerMemberNameAttribute を提供します。これにより、プロパティ名を文字列として渡す必要がなくなります。

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 ...
}

KB2468871 がインストールされたFramework 4.0で作業している場合は、Microsoft BCL互換機能パックnuget 、これもこの属性を提供します。

24
takrl

このブログ この記事は、このアプローチのパフォーマンスについていくつかの良い質問を提起します 。静的な初期化の一部として式を文字列に変換することで、これらの欠点を改善できます。

実際のメカニズムは少し見苦しいかもしれませんが、それでもタイプセーフであり、未加工のINotifyPropertyChangedとほぼ同じパフォーマンスです。

このようなもの:

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);
        }
    }
}
5
nedruod

バインディングが壊れている場合にフィードバックを取得する1つの方法は、DataTemplateを作成し、そのDataTypeを、バインドするViewModelのタイプとして宣言することです。 PersonViewとPersonViewModelがある場合は、次のようにします。

  1. DataType = PersonViewModelとキーを使用してDataTemplateを宣言します(例:PersonTemplate)

  2. すべてのPersonView xamlを切り取り、それをデータテンプレートに貼り付けます(理想的には、PersonViewの最上部に配置できます。

3a。 ContentControlを作成してContentTemplate = PersonTemplateを設定し、そのコンテンツをPersonViewModelにバインドします。

3b。別のオプションは、DataTemplateにキーを与えず、ContentControlのContentTemplateを設定しないことです。この場合、WPFはバインドするオブジェクトの種類を認識しているため、どのDataTemplateを使用するかを判断します。ツリーを検索してDataTemplateを見つけ、バインディングのタイプと一致するため、ContentTemplateとして自動的に適用します。

基本的には以前と同じビューになりますが、DataTemplateを基になるDataTypeにマップしたので、Resharperなどのツールは、バインディングが壊れているかどうかについて(色識別子-Resharper-Options-Settings-Color Identifiersを介して)フィードバックを提供できます。か否か。

それでもコンパイラの警告は表示されませんが、壊れたバインディングを視覚的に確認できます。これは、ビューとビューモデルの間を行き来することよりも優れています。

この追加情報のもう1つの利点は、リファクタリングの名前変更にも使用できることです。私が覚えている限り、Resharperは、基になるViewModelのプロパティ名が変更されたときに、型指定されたDataTemplatesのバインディングの名前を自動的に変更できます。

3
Thorsten Lorenz

1.プロパティが削除または名前変更された場合、コンパイラの警告は表示されません。

2.リファクタリングツールを使用してプロパティの名前を変更した場合、データバインディングが更新されない可能性があります。

3.プロパティのタイプが間違っている場合、実行時までエラーが発生しません。整数を日付チューザにバインドします。

はい、Ian、それはまさに名前文字列駆動のデータバインディングに関する問題です。あなたはデザインパターンを求めました。 Model-View-ViewModel(MVVM)パターンのView Model部分をまとめたタイプセーフビューモデル(TVM)パターンを設計しました。それはあなた自身の答えに似たタイプセーフなバインディングに基づいています。 WPFのソリューションを投稿しました。

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

3

windows 10およびWindows Phone 10でのXAML(ユニバーサルアプリ)のx:bind(「コンパイル済みデータバインディング」とも呼ばれます)は、この問題を解決する可能性があります。 https://channel9.msdn.com/Events/Build/2015)を参照してください。/3-635

オンラインドキュメントは見つかりませんが、しばらく使用しないので、あまり力を入れていません。しかし、この答えは他の人々への有用な指針になるはずです。

1
Ian Ringrose