Como fazer o tipo de binding de dados seguro e suporte a refatoração

Quando desejo ligar um controle a uma propriedade do meu object, tenho que fornecer o nome da propriedade como uma string. Isso não é muito bom porque:

  1. Se a propriedade for removida ou renomeada, não receberei um aviso do compilador.
  2. Se renomear a propriedade com uma ferramenta de refatoração, é provável que a binding de dados não seja atualizada.
  3. Eu não obtenho um erro até o tempo de execução se o tipo da propriedade estiver errado, por exemplo, vinculando um inteiro a um seletor de datas.

Existe um padrão de design que contorna isso, mas ainda tem a facilidade de usar a vinculação de dados?

(Este é um problema no WinForm, Asp.net e WPF e, provavelmente, muitos outros sistemas)

Eu tenho encontrado agora ” soluções alternativas para o operador nameof () em C #: binding de dados typesafe “, que também tem um bom ponto de partida para uma solução.

Se você estiver disposto a usar um pós-processador depois de compilar seu código, notifique bem o que é o lookpropertyweaver .


Alguém sabe de uma boa solução para o WPF quando as ligações são feitas em XML, em vez de C #?

Graças ao Oliver por me iniciar, agora tenho uma solução que suporta refatoração e é segura. Ele também permite implementar o INotifyPropertyChanged para que ele copie as propriedades que estão sendo renomeadas.

Seu uso parece com:

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

A class de pessoa mostra como implementar INotifyPropertyChanged de uma maneira segura para o tipo (ou veja essa resposta para uma outra maneira bastante agradável de implementar INotifyPropertyChanged, ActiveSharp – Automatic INotifyPropertyChanged também parece ser bom):

 public class Person : INotifyPropertyChanged { private bool _employed; public bool Employed { get { return _employed; } set { _employed = value; OnPropertyChanged(() => c.Employed); } } // etc private void OnPropertyChanged(Expression> property) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(BindingHelper.Name(property))); } } public event PropertyChangedEventHandler PropertyChanged; } 

A class auxiliar de binding do WinForms tem a carne que faz tudo funcionar:

 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(Expression> expression) { return GetMemberName(expression.Body); } //NEW public static string Name(Expression> expression) { return GetMemberName(expression.Body); } public static void Bind(this TC control, Expression> controlProperty, TD dataSource, Expression> dataMember) where TC : Control { control.DataBindings.Add(Name(controlProperty), dataSource, Name(dataMember)); } public static void BindLabelText(this Label control, T dataObject, Expression> dataMember) { // as this is way one any type of property is ok control.DataBindings.Add("Text", dataObject, Name(dataMember)); } public static void BindEnabled(this Control control, T dataObject, Expression> dataMember) { control.Bind(c => c.Enabled, dataObject, dataMember); } } } 

Isso faz uso de muitas das novas coisas no C # 3.5 e mostra o que é possível. Agora, se tivéssemos macros higiênicos, o programador lisp pode parar de nos chamar de cidadãos de segunda class)

O operador nameof foi implementado no C # 6.0 com .NET 4.6 e VS2015 em julho de 2015. O seguinte ainda é válido para C # <6.0

Para evitar sequências que contenham nomes de propriedades, escrevi uma class simples usando trees de expressão para retornar o nome do 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(Expression> expression) { return GetMemberName(expression.Body); } public static string Name(Expression> expression) { return GetMemberName(expression.Body); } } 

Você pode usar essa class da seguinte maneira. Mesmo que você possa usá-lo apenas no código (portanto, não no XAML), é bastante útil (pelo menos para mim), mas seu código ainda não é seguro de digitar. Você poderia estender o método Name com um segundo argumento de tipo que define o valor de retorno da function, o que restringiria o tipo da propriedade.

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

Até agora não encontrei nada que resolva o problema de segurança de tipos de binding de dados.

Cumprimentos

O Framework 4.5 nos fornece o CallerMemberNameAttribute , que torna desnecessário passar o nome da propriedade como uma string:

 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 você estiver trabalhando no Framework 4.0 com KB2468871 instalado, poderá instalar o Pacote de Compatibilidade da Microsoft BCL via nuget , que também fornece esse atributo.

Este artigo do blog levanta algumas boas perguntas sobre o desempenho dessa abordagem . Você poderia melhorar essas deficiências convertendo a expressão em uma string como parte de algum tipo de boot estática.

A mecânica real pode ser um pouco desagradável, mas ainda assim seria segura para o tipo e aproximadamente igual ao desempenho bruto do INotifyPropertyChanged.

Algo parecido com isto:

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

Uma maneira de obter feedback se suas ligações forem quebradas é criar um DataTemplate e declarar seu DataType como o tipo de ViewModel ao qual ele se liga, por exemplo, se você tiver um PersonView e um PersonViewModel, faça o seguinte:

  1. Declare um DataTemplate com DataType = PersonViewModel e uma chave (por exemplo, PersonTemplate)

  2. Corte todo o PersonView xaml e cole-o no modelo de dados (que idealmente pode estar no topo do PersonView.

3a. Crie um ContentControl e defina o ContentTemplate = PersonTemplate e vincule seu conteúdo ao PersonViewModel.

3b. Outra opção é não dar uma chave ao DataTemplate e não definir o ContentTemplate do ContentControl. Neste caso, o WPF irá descobrir o que DataTemplate usar, desde que ele saiba que tipo de object você está ligando. Ele pesquisará a tree e localizará seu DataTemplate e, desde que corresponda ao tipo da binding, aplicará automaticamente como o ContentTemplate.

Você acaba tendo essencialmente a mesma visão de antes, mas desde que você mapeou o DataTemplate para um DataType subjacente, ferramentas como Resharper podem lhe dar feedback (via identificadores de colors – Resharper-Options-Settings-Color Identifiers) quanto a suas ligações serem quebradas ou não.

Você ainda não receberá avisos do compilador, mas pode verificar visualmente se há ligações quebradas, o que é melhor do que ter que alternar entre a visualização e o viewmodel.

Outra vantagem dessa informação adicional que você fornece é que ela também pode ser usada para renomear refatorações. Tanto quanto eu me lembro Resharper é capaz de renomear automaticamente as ligações em DataTemplates typescripts quando o nome da propriedade subjacente ViewModel é alterado e vice-versa.

1.Se a propriedade for removida ou renomeada, não receberei um aviso de compilador.

2.Se renomear a propriedade com uma ferramenta de refatoração, é provável que a vinculação de dados não seja atualizada.

3. Eu não obtenho um erro até o tempo de execução se o tipo da propriedade estiver errado, por exemplo, ligando um inteiro a um seletor de data.

Sim, Ian, são exatamente os problemas com a vinculação de dados orientada por cadeia de nomes. Você pediu um padrão de design. Eu projetei o padrão Type-Safe View Model (TVM) que é uma concreção da parte View Model do padrão Model-View-ViewModel (MVVM). É baseado em uma binding segura para o tipo, semelhante à sua própria resposta. Acabei de postar uma solução para o WPF:

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

x: bind (também chamado de “compilado ligações de dados”) para XAML (aplicativo universal) no Windows 10 e Windows Phone 10 pode resolver este problema, consulte https://channel9.msdn.com/Events/Build/2015/3-635

Não consigo encontrar os documentos on-line para isso, mas não tenho feito muito esforço, pois é algo que eu não usarei por algum tempo. No entanto, esta resposta deve ser um indicador útil para outras pessoas.