framework - En MVVM con WPF, ¿cómo puedo probar en unidad el enlace entre ViewModel y View?
mvvm tutorial (5)
En MVVM es normal conectar la Vista al ViewModel con enlace de datos.
Por lo tanto, si el nombre de una propiedad cambia en uno de los objetos del modelo que está enlazado a una base de datos, no hay error de compilación.
Cuando el compilador no detiene un error, lo siguiente que pienso es en "UnitTest", sin embargo
¿Cómo puedes probar esto sin gastar para siempre escribir una prueba de GUI?
¿Hay algún sistema que verifique que todas las propiedades vinculadas sean válidas (sin tener que ejecutar la interfaz de usuario) a las que puedo llamar en una prueba de unidad?
Estoy buscando algo que tome la vista, y luego recorra todos los controles de WPF, para cada control de WPF verá todos los enlaces y verificará si son válidos.
Por cierto, ha habido algunas buenas preguntas sobre cómo hacer que OnPropertyChanged sea seguro y / o cómo probarlo (pero al final de todo esto se reduce al nivel de una vista WPF).
- Cómo hacer que Databinding sea seguro y soporta refactorización.
- Automáticamente INotifyPropertyChanged
- soluciones para el operador nameof () en C #: enlace de datos seguro de tipos
- Una interfaz fluida para probar INotifyPropertyChanged
- Automatic Class Tester probará todas las propiedades simples y INotifyPropertyChanged
He dado una recompensa a esta pregunta, ya que alguien debe haber pensado mucho sobre el problema y llegar a soluciones.
Como señala Anvaka, el uso de una clase base para su modelo de vista que verifica los nombres de propiedad puede ayudar a evitar este problema en particular (aunque no le dirá cuándo su clase VM realiza su propia notificación de cambio de propiedad e ignora el método en la clase base , no es que alguna vez haya visto algo así en mi código).
Y puede (y debe) instrumentar su código para que las cosas que no funcionan falle de una manera que sea visible para usted. Lo paradójico de esto es que si sabes qué cosas pueden fallar y las miras, no lo harán, porque el hecho de que las estés observando evitará que cometas los errores que las llevan a fallar (como escribiendo un selector de plantilla que no siempre devuelve una plantilla).
Pero fundamentalmente, la vista es la interfaz de usuario, por lo que me sorprendería encontrar métodos de prueba que no fueran también métodos para probar la interfaz de usuario.
Muy buena pregunta. Lo voté arriba. Me gustaría saber la respuesta también.
Una de las mejores prácticas que conozco ( sugerida por Josh Smith , gracias a Gishu por señalarlo) es tener una clase de modelo de vista base para verificar en el método OnPropertyChanged()
si la propiedad realmente existe. P.ej:
abstract class ViewModelBase
{
[Conditional("DEBUG")]
public void VerifyPropertyName(string propertyName)
{
// Verify that the property name matches a real,
// public, instance property on this object.
if (TypeDescriptor.GetProperties(this)[propertyName] == null)
{
if (this.ThrowOnInvalidPropertyName)
throw new ArgumentException(propertyName);
string msg = "Invalid property name: " + propertyName;
Debug.Fail(msg);
}
}
protected void OnPropertyChanged(string propertyName)
{
VerifyPropertyName(propertyName);
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
}
}
Pero esto no te ayudaría a encontrar problemas de ortografía en XAML. Hmm ... No sé ninguna solución existente para esto. Tal vez los chicos de WPF Disciples podrían sugerir algo. Creo que iría con PresentationTraceSources.DataBindingSource y agregaría a su instancia de la colección TextWriterTraceListener de TextWriterTraceListener y luego monitorearía la salida. Tan pronto como recibamos un error o una advertencia en nuestro radar, deberíamos fallar una prueba.
Buen ejemplo encontrado: Fragmento de WPF - Detección de errores de enlace
Espero que esto ayude. Al menos un poco :).
Saludos, Anvaka.
Sé que esta no es la respuesta directa a tu pregunta.
Si conoce el nombre del elemento de control, que espera estar vinculado en su contra, puede hacer algo como la prueba a continuación (usando nunit). Esta es la versión aproximada. Pero aquí usa expresiones y comprueba explícitamente que la propiedad está en un enlace
[Test]
public void TestBindings()
{
TestBinding<IndividualSolutionViewModel, string>(x => x.Name, "Name", TextBlock.TextProperty);
}
private void TestBinding<TViewModel,TResult>(Expression<Func<TViewModel, TResult>> property, string elementName,
DependencyProperty dependencyProperty)
{
string memberName = ExpressionHelper.GetPropertyName(property); // f.ex v => v.Name will return Name
TestBinding(memberName, elementName, dependencyProperty);
}
private void TestBinding(string memberName, string elementInControlName, DependencyProperty dependencyProperty)
{
//object viewModel = new IndividualSolutionViewModel();
var view = new IndividualSolutionView();
//Assert.That(view.DataContext, Is.EqualTo(viewModel));
var element = view.FindName(elementInControlName);
Assert.That(element, Is.Not.Null, string.Format("Unable to find the element {0} in view {1}", elementInControlName, view.Name));
Assert.That(element, Is.InstanceOf(typeof(DependencyObject)));
var binding = BindingOperations.GetBinding(element as DependencyObject, dependencyProperty);
Assert.That(binding, Is.Not.Null, string.Format("Could not find a binding for the control {0}", elementInControlName));
Assert.That(binding.Path.Path, Is.EqualTo(memberName));
}
PD. Tienes que agregar esto a la app.config
<configSections>
<sectionGroup name="NUnit">
<section type="System.Configuration.NameValueSectionHandler"
name="TestRunner"></section>
</sectionGroup>
</configSections>
<NUnit>
<TestRunner>
<add value="STA" key="ApartmentState"></add>
</TestRunner>
</NUnit>
También existe esta posibilidad, que podría darle algunas ideas. La esencia de la idea es que los nombres de propiedades a los que estaría vinculado se exponen como propiedades de cadena estática. Si un nombre de propiedad de enlace cambia, obtendrías un error de compilación.
No he tenido la oportunidad de probar esta técnica, pero me parece interesante:
http://www.codeproject.com/Articles/42036/Project-Metadata-Generation-using-T4
Creo que se me ha ocurrido algo que puede funcionar utilizando una reflexión simple y adaptando un código que he usado en el pasado (el código para el método FindBindingsRecursively
fue escrito por Martin Bennedik''s como parte de su Control de Validación de Enterprise WPF ):
[TestMethod]
public void CheckWpfBindingsAreValid()
{
// instansiate the xaml view and set DataContext
var yourView = new YourView();
yourView.DataContext = YourViewModel;
FindBindingsRecursively(yourView,
delegate(FrameworkElement element, Binding binding, DependencyProperty dp)
{
var type = yourView.DataContext.GetType();
// check that each part of binding valid via reflection
foreach (string prop in binding.Path.Path.Split(''.''))
{
PropertyInfo info = type.GetProperty(prop);
Assert.IsNotNull(info);
type = info.PropertyType;
}
});
}
private delegate void FoundBindingCallbackDelegate(FrameworkElement element, Binding binding, DependencyProperty dp);
private void FindBindingsRecursively(DependencyObject element, FoundBindingCallbackDelegate callbackDelegate)
{
// See if we should display the errors on this element
MemberInfo[] members = element.GetType().GetMembers(BindingFlags.Static |
BindingFlags.Public |
BindingFlags.FlattenHierarchy);
foreach (MemberInfo member in members)
{
DependencyProperty dp = null;
// Check to see if the field or property we were given is a dependency property
if (member.MemberType == MemberTypes.Field)
{
FieldInfo field = (FieldInfo)member;
if (typeof(DependencyProperty).IsAssignableFrom(field.FieldType))
{
dp = (DependencyProperty)field.GetValue(element);
}
}
else if (member.MemberType == MemberTypes.Property)
{
PropertyInfo prop = (PropertyInfo)member;
if (typeof(DependencyProperty).IsAssignableFrom(prop.PropertyType))
{
dp = (DependencyProperty)prop.GetValue(element, null);
}
}
if (dp != null)
{
// Awesome, we have a dependency property. does it have a binding? If yes, is it bound to the property we''re interested in?
Binding bb = BindingOperations.GetBinding(element, dp);
if (bb != null)
{
// This element has a DependencyProperty that we know of that is bound to the property we''re interested in.
// Now we just tell the callback and the caller will handle it.
if (element is FrameworkElement)
{
callbackDelegate((FrameworkElement)element, bb, dp);
}
}
}
}
// Now, recurse through any child elements
if (element is FrameworkElement || element is FrameworkContentElement)
{
foreach (object childElement in LogicalTreeHelper.GetChildren(element))
{
if (childElement is DependencyObject)
{
FindBindingsRecursively((DependencyObject)childElement, callbackDelegate);
}
}
}
}