data binding - porque - ¿MVVM carga datos durante o después de la construcción de ViewModel?
xamarin mvc (5)
Mi pregunta genérica es como dice el título, ¿es mejor cargar datos durante la construcción de ViewModel o después a través de algún manejo de evento cargado?
Supongo que la respuesta es después de la construcción a través de algún manejo de evento cargado, pero me pregunto cómo se coordina más limpiamente entre ViewModel y View.
Aquí hay más detalles sobre mi situación y el problema particular que estoy tratando de resolver:
Estoy usando el framework MVVM Light así como también Unity para DI. Tengo algunas Vistas anidadas, cada una vinculada a un ViewModel correspondiente. Los ViewModels están vinculados al DataContext del control de raíz de cada Vista a través de la idea ViewModelLocator que Laurent Bugnion ha puesto en MVVM Light. Esto permite encontrar ViewModels a través de un recurso estático y controlar la vida útil de ViewModels a través de un marco de inyección de dependencias, en este caso Unity. También permite que Expression Blend vea todo lo relacionado con ViewModels y cómo vincularlos.
De todos modos, tengo una Vista padre que tiene un ComboBox de datos a un ObservableCollection en su ViewModel. El elemento seleccionado de ComboBox también está vinculado (bidireccional) a una propiedad en ViewModel. Cuando la selección del ComboBox cambia, esto es para activar actualizaciones en otras vistas y subvistas. Actualmente estoy logrando esto a través del sistema de mensajería que se encuentra en MVVM Light. Todo esto funciona muy bien y como se esperaba cuando eliges diferentes elementos en el ComboBox.
Sin embargo, ViewModel obtiene sus datos durante el tiempo de construcción a través de una serie de llamadas a métodos de inicialización. Esto parece ser solo un problema si quiero controlar lo que es el SelectedItem inicial de ComboBox. Utilizando el sistema de mensajería de MVVM Light, actualmente lo configuro donde el colocador de la propiedad SelectedItem de ViewModel es el que difunde la actualización y los otros ViewModels interesados se registran para el mensaje en sus constructores. Parece que actualmente estoy intentando establecer SelectedItem a través del ViewModel en el momento de la construcción, lo que no ha permitido que se construyan sub-ViewModels y se registren todavía.
¿Cuál sería la forma más limpia de coordinar la carga de datos y la configuración inicial de SelectedItem dentro de ViewModel? Realmente quiero seguir poniendo tan poco en el código subyacente de View como sea razonable. Creo que solo necesito una forma para que ViewModel sepa cuando las cosas se han cargado y que luego puede continuar cargando los datos y finalizando la fase de configuración.
Gracias de antemano por sus respuestas.
Decidí tener el XAML vinculado de forma declarativa a un controlador de eventos cargado en el código subyacente de la vista, que a su vez acaba de llamar a un método en el objeto ViewModel, a través del elemento raíz de la vista UserControl DataContext.
Era una solución bastante simple, directa y limpia. Supongo que esperaba una forma de vincular el evento Loaded al objeto ViewModel de la misma manera declarativa con ICommands en XAML.
Puede que le haya dado a Klinger el crédito de respuesta oficial, pero ha publicado un comentario sobre mi pregunta y no una respuesta. Así que al menos le di un comentario sobre su comentario.
La siguiente solución es similar a la ya provista y aceptada, pero no usa un comando en el modelo de vista para cargar los datos, sino un "método normal". Creo que los comandos son más adecuados para las acciones del usuario (los comandos pueden estar disponibles y no están disponibles en el tiempo de ejecución), es por eso que se usa una llamada a método regular, pero también se establece un activador de interacción en la vista.
Sugiero esto: crear una clase de modelo de vista. Cree una instancia de la clase de modelo de vista dentro de la xaml de la vista dentro de la propiedad DataContext
.
Implemente un método para cargar los datos en su modelo de vista, por ejemplo LoadData
. Configure la vista para que se llame a este método cuando se carga la vista. Esto se realiza mediante un activador de interacción en su vista que está vinculado al método en el modelo de vista (se necesitan referencias a "Microsoft.Expression.Interactions" y "System.Windows.Interactivity"):
Ver (xaml):
<Window x:Class="MyWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Test"
xmlns:viewModel="clr-namespace:ViewModels"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
>
<Window.DataContext>
<viewModel:ExampleViewModel/>
</Window.DataContext>
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<ei:CallMethodAction TargetObject="{Binding}" MethodName="LoadData"/>
</i:EventTrigger>
</i:Interaction.Triggers>
Esto llamará al método LoadData
en ViewModel en tiempo de ejecución cuando se carga la vista. Aquí es donde cargas tus datos.
public class ExampleViewModel
{
/// <summary>
/// Constructor.
/// </summary>
public ExampleViewModel()
{
// Do NOT do complex stuff here
}
public void LoadData()
{
// Make a call to the repository class here
// to set properties of your view model
}
Si el método en el repositorio es un método asíncrono, también puede hacer que el método LoadData
asincrónico, pero esto no es necesario en cada caso.
Por cierto, generalmente no cargaría datos en el constructor del modelo de vista. En el ejemplo anterior, se llama al constructor (parámetro menos) del modelo de vista cuando el diseñador muestra su vista. Hacer cosas complejas aquí puede causar errores en el diseñador al mostrar su vista (por la misma razón no haría cosas complejas en el constructor de vistas).
En algunos escenarios, el código en el constructor de modelos de vista puede causar problemas en el tiempo de ejecución, cuando los constructores de modelos de vista se ejecutan, establece propiedades del modelo de vista que están vinculadas a elementos en la vista, mientras que el objeto de vista no está completamente terminado de crearse.
OK entonces. :-)
Puede vincular a un método en ViewModel mediante el uso de un comportamiento.
Aquí hay un enlace que te ayudará con eso. http://expressionblend.codeplex.com/
Para eventos, debe usar EventToCommand en MVVM Light Toolkit. Usando esto puedes vincular cualquier evento de cualquier elemento ui a un comando de retransmisión. Echa un vistazo a su artículo sobre EventToCommand en
Descargue la muestra y eche un vistazo. Es genial. No necesitarás ningún código detrás de eso. Un ejemplo es el siguiente:
<Page x:Class="cubic.cats.Wpf.Views.SplashScreenView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
Title="SplashScreenPage">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<cmd:EventToCommand Command="{Binding LoadedCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<Grid>
<Label Content="This is test page" />
</Grid>
</Page>
y el modo de vista podría ser así
public class SplashScreenViewModel : ViewModelBase
{
public RelayCommand LoadedCommand
{
get;
private set;
}
/// <summary>
/// Initializes a new instance of the SplashScreenViewModel class.
/// </summary>
public SplashScreenViewModel()
{
LoadedCommand = new RelayCommand(() =>
{
string a = "put a break point here to see that it gets called after the view as been loaded";
});
}
}
si desea que el modelo de vista tenga los EventArgs, puede configurar PassEventArgsToCommand en true:
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<cmd:EventToCommand PassEventArgsToCommand="True" Command="{Binding LoadedCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
y el modelo de vista será como
public class SplashScreenViewModel : ViewModelBase
{
public RelayCommand<MouseEventArgs> LoadedCommand
{
get;
private set;
}
/// <summary>
/// Initializes a new instance of the SplashScreenViewModel class.
/// </summary>
public SplashScreenViewModel()
{
LoadedCommand = new RelayCommand<MouseEventArgs>(e =>
{
var a = e.WhateverParameters....;
});
}
}
Tuve el mismo problema al tratar con mensajes entre una ventana principal y una ventana secundaria. Simplemente cambie el orden en que se crean sus modelos de vista en su clase ViewModelLocator. Asegúrese de que todos los modelos de vista que dependen de un mensaje se creen antes del modelo de vista que envía el mensaje.
Por ejemplo, en el constructor de su clase ViewModelLocator:
public ViewModelLocator()
{
if (s_messageReceiverVm == null)
{
s_messageReceiverVm = new MessageReceiverVM();
}
if (s_messageSenderVm == null)
{
s_messageSenderVm = new MessageSenderVM();
}
}