Cómo hacer que el tipo de enlace de datos sea seguro y admita la refactorización
-
19-09-2019 - |
Pregunta
Cuando deseo vincular un control a una propiedad de mi objeto, debo proporcionar el nombre de la propiedad como una cadena.Esto no es muy bueno porque:
- Si la propiedad se elimina o se renombra, no recibo una advertencia de compilador.
- Si un cambio de nombre de la propiedad con una herramienta de refactorización, es probable que no se actualice el enlace de datos.
- No recibo un error hasta el tiempo de ejecución si el tipo de propiedad está mal, por ejemplo,vinculando a un entero a un selector de citas.
¿Existe algún patrón de diseño que solucione esto, pero que aún tenga la facilidad de uso del enlace de datos?
(Este es un problema en WinForm, Asp.net y WPF y probablemente en muchos otros sistemas)
Ahora he encontrado "Soluciones alternativas para el operador nameof() en C#:enlace de datos con seguridad de tipos"Esto también es un buen punto de partida para una solución.
Si está dispuesto a utilizar un posprocesador después de compilar su código, notificarpropertyweaver Vale la pena mirarlo.
¿Alguien conoce una buena solución para WPF cuando los enlaces se realizan en XML en lugar de C#?
Solución
Gracias a Oliver por ayudarme a empezar, ahora tengo una solución que admite la refactorización y es segura para escribir.También me permitió implementar INotifyPropertyChanged para que pueda hacer frente al cambio de nombre de las propiedades.
Su uso es el siguiente:
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 clase de persona muestra cómo implementar INotifyPropertyChanged de una manera segura (o ver esta respuesta para conocer otra forma bastante agradable de implementar INotifyPropertyChanged, ActiveSharp: INotifyPropertyChanged automático también se ve bien):
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 clase auxiliar de enlace de WinForms tiene la esencia que hace que todo funcione:
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);
}
}
}
Esto hace uso de muchas de las novedades de C# 3.5 y muestra lo que es posible.Ahora si tan solo tuviéramos macros higiénicas el programador lisp puede dejar de llamarnos ciudadanos de segunda clase)
Otros consejos
El operador nombredel fue implementado en C # 6.0 con .NET 4.6 y VS2015 en julio de 2015. El siguiente es todavía válida para C # <6,0
Para evitar cadenas que contienen los nombres de propiedades, he escrito una clase simple usando los árboles de expresión para devolver el nombre del miembro:
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);
}
}
Puede utilizar esta clase de la siguiente manera. A pesar de que se puede usar solamente en el código (por lo que no en XAML), es bastante útil (al menos para mí), pero el código aún no está typesafe. Se podría extender el método Nombre con un segundo argumento que define el tipo de valor de retorno de la función, lo que limitaría el tipo de la propiedad.
var name = Member.Name<MyClass>(x => x.MyProperty); // name == "MyProperty"
Hasta ahora no he encontrado nada que resuelve el problema de enlace de datos typesafety.
Saludos
El Framework 4.5 nos proporciona la CallerMemberNameAttribute
, que hace pasar el nombre de la propiedad como una cadena innecesaria:
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 ...
}
Si usted está trabajando en Framework 4.0 con KB2468871 instalado, puede instalar el Microsoft BCL Paquete de compatibilidad a través de Nuget , que también proporciona este atributo.
Este artículo rel="noreferrer"> href="http://blog.quantumbitdesigns.com/2010/01/26/mvvm-lambda-vs-inotifypropertychanged-vs-dependencyobject/" el blog . Se podría mejorar esas deficiencias mediante la conversión de la expresión de una cadena como parte de algún tipo de inicialización estática.
La mecánica real podría ser un poco desagradable, pero seguirían siendo de tipo seguro, y aproximadamente el mismo rendimiento a la INotifyPropertyChanged prima.
Algo algo así como esto:
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);
}
}
}
Una forma de obtener retroalimentación si sus enlaces se rompen, es crear un DataTemplate y declarar su tipo de datos a ser el tipo de modelo de vista que se une a, por ejemplo si tiene un PersonView y una PersonViewModel que haría lo siguiente:
-
Declarar una DataTemplate con DataType = PersonViewModel y una clave (por ejemplo PersonTemplate)
-
Cortar todas xaml PersonView y pegarlo en la plantilla de datos (que idealmente solo puede ser en la parte superior de la PersonView.
3a. Crear un ContentControl y establecer el ContentTemplate = PersonTemplate y unirse a su contenido a la PersonViewModel.
3b. Otra opción es no dar una clave para la DataTemplate y no establecer el ContentTemplate del ContentControl. En este caso WPF se darán cuenta de lo que DataTemplate de usar, ya que sabe qué tipo de objeto que realizar la vinculación. Se buscará el árbol y encontrar su DataTemplate y ya que coincide con el tipo de la unión, se aplicará automáticamente como ContentTemplate.
Se termina con esencialmente la misma vista que antes, pero desde que ha correlacionado la DataTemplate a un tipo de datos subyacente, herramientas como ReSharper puede dar información (a través de identificadores de color - ReSharper-Opciones-Configuración-color Identificadores) como a wether su enlaces se rompen o no.
Todavía no recibirá advertencias del compilador, pero puede comprobar visualmente para los enlaces rotos, lo cual es mejor que tener que comprobar ida y vuelta entre la vista y el modelo de vista.
Otra de las ventajas de esta información adicional se da, es decir, que también puede ser utilizado en el cambio de nombre refactorizaciones. Por lo que yo recuerdo ReSharper es capaz de nombre fijaciones en DataTemplates mecanografiadas cuando el nombre de la propiedad del modelo de vista subyacente se cambia y viceversa.
1. Si la propiedad es eliminado o cambiado de nombre, no consigo una advertencia del compilador.
2. Si un cambio de nombre de la propiedad con una herramienta de refactorización, es probable que el enlace de datos no se actualizará.
3.He no recibe un error hasta que el tiempo de ejecución si el tipo de la propiedad es incorrecta, por ejemplo, la unión de un número entero a un selector de fechas.
Sí, Ian, que son exactamente los problemas con el enlace de datos nombre-cadena accionadas. Usted pidió un diseño de patrón. Diseñé el patrón de tipo seguro Ver Modelo (TVM) que es una concreción de la pieza Ver Modelo del patrón Model-View-ViewModel (MVVM). Se basa en un tipo de fallos de enlace, similar a su propia respuesta. Yo sólo he publicado una solución para WPF:
http: // www.codeproject.com/Articles/450688/Enhanced-MVVM-Design-w-Type-Safe-View-Models-TVM
x: bind (también llamados "enlaces de datos compilados") para XAML (aplicación universal) en Windows 10 y Windows Phone 10 puede resolver este problema, consulte https://channel9.msdn.com/Events/Build/2015/3-635
No puedo encontrar la documentación en línea para ella, pero no he puesto mucho esfuerzo en el, ya que es algo que no va a utilizar durante algún tiempo. Sin embargo, esta respuesta debe ser un indicador útil para otras personas.