visual tutorial studio hechas fuente español ejemplos desde crear codigo cero arquitectura aprender aplicaciones aplicacion wpf xaml expression-blend

tutorial - Expression Blend y datos de muestra para el diccionario en la aplicación WPF



wpf c# tutorial (3)

Tengo una aplicación WPF que estoy usando Blend to style.

Uno de mis modelos de vista es del tipo:

public Dictionary<DateTime, ObservableCollection<MyViewModel>> TimesAndEvents

Pero cuando intento crear algunos datos de muestra en Expression Blend, simplemente no se crea el XAML para esta propiedad.

¿Puedes crear un tipo de datos como este en XAML? El soporte de tiempo no diseñado está matando mi productividad.


Además, he seguido la ruta de crear una instancia de tiempo de diseño de mi Viewmodel en mi Localizador a la que me refiero como @ChrisW sugerido anteriormente:

d:DataContext="{Binding Source={StaticResource Locator}, Path=DesignTimeVM}"

Así que puedo tener algunos valores codificados para poblar mis listas, cuadros combinados, etc. Hace que el estilo de todo sea mucho más fácil.

Uso MVVM Light y, por lo tanto, en el constructor de mi ViewModel, uso un patrón como este:

if(IsInDesignMode) { ListUsers = new List<User>(); . . . }

El código solo se ejecutará en tiempo de diseño, y tendrá su Xaml UI vinculada a los datos reales.


Dado que Xaml 2009 admite tipos genéricos, es posible escribir un xaml suelto (no se puede compilar en el proyecto wpf) de esta manera para representar un diccionario.

Data.xaml

<gnrc:Dictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=mscorlib" xmlns:gnrc="clr-namespace:System.Collections.Generic;assembly=mscorlib" xmlns:om="clr-namespace:System.Collections.ObjectModel;assembly=System" x:TypeArguments="sys:DateTime,om:ObservableCollection(x:String)"> <om:ObservableCollection x:TypeArguments="x:String"> <x:Key> <sys:DateTime>2017/12/31</sys:DateTime> </x:Key> <x:String>The last day of the year.</x:String> <x:String>Party with friends.</x:String> </om:ObservableCollection> <om:ObservableCollection x:TypeArguments="x:String"> <x:Key> <sys:DateTime>2018/1/1</sys:DateTime> </x:Key> <x:String>Happy new year.</x:String> <x:String>Too much booze.</x:String> </om:ObservableCollection> <om:ObservableCollection x:TypeArguments="x:String"> <x:Key> <sys:DateTime>2018/1/10</sys:DateTime> </x:Key> <x:String>Just another year.</x:String> <x:String>Not much difference.</x:String> </om:ObservableCollection> </gnrc:Dictionary>

Pero no es compatible con diseñadores como Blend o Visual Studio. Si lo pones en un xaml asociado con un diseñador, obtendrás decenas de errores. Para resolver esto, necesitamos una extensión de marcado para proporcionar valor desde Data.xaml mediante el uso del método XamlReader.Load.

InstanceFromLooseXamlExtension.cs

public class InstanceFromLooseXamlExtension : MarkupExtension { public Uri Source { get; set; } public override object ProvideValue(IServiceProvider serviceProvider) { if (Source == null) { throw new ArgumentNullException(nameof(Source)); } Uri source; if (Source.IsAbsoluteUri) { source = Source; } else { var iuc = serviceProvider?.GetService(typeof(IUriContext)) as IUriContext; if (iuc == null) { throw new ArgumentException("Bad service contexts.", nameof(serviceProvider)); } source = new Uri(iuc.BaseUri, Source); } WebResponse response; if (source.IsFile) { response = WebRequest.Create(source.GetLeftPart(UriPartial.Path)).GetResponse(); } else if(string.Compare(source.Scheme, PackUriHelper.UriSchemePack, StringComparison.Ordinal) == 0) { var iwrc = new PackWebRequestFactory() as IWebRequestCreate; response = iwrc.Create(source).GetResponse(); } else { throw new ArgumentException("Unsupported Source.", nameof(Source)); } object result; try { result = XamlReader.Load(response.GetResponseStream()); } finally { response.Close(); } return result; } }

Esta extensión de marca tiene una propiedad Fuente de tipo Uri para permitir al usuario especificar qué archivo xaml cargar. Entonces, finalmente, use la extensión de marcado como esta.

MainWindow.xaml

<Window x:Class="WpfApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:WpfApp" mc:Ignorable="d" Title="MainWindow" Height="350" Width="525"> <ListBox ItemsSource="{local:InstanceFromLooseXaml Source=/Data.xaml}"> <ListBox.ItemTemplate> <DataTemplate> <Expander Header="{Binding Key}"> <ListBox ItemsSource="{Binding Value}"/> </Expander> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Window>

En este caso, coloco Data.xaml en la carpeta de la aplicación, por lo que ''Source = / Data.xaml'' estará bien. Cada vez que el diseñador recargue (una reconstrucción lo garantizará), se aplicará el contenido en xaml suelto. El resultado debería verse como

El xaml suelto puede contener casi todo, como un ResourceDictionary o algo con UiElements. Pero tanto Blend como Visual Studio no lo verifican correctamente por ti. Al final, espero que esto sea suficiente para una respuesta.


Con respecto a su última pregunta: desafortunadamente, no puede crear fácilmente una instancia de los diccionarios en WPF. Creo que esta respuesta explica bien esa parte. El libro, WPF 4.5 Unleashed, proporciona un buen resumen de lo que dice la respuesta vinculada:

Una solución común para esta limitación (no poder instanciar un diccionario en la versión de WPF de XAML) es derivar una clase no genérica de una genérica simplemente para que se pueda hacer referencia a ella desde XAML ...

Pero incluso entonces, crear una instancia de ese diccionario en xaml es nuevamente, en mi opinión, un proceso doloroso. Además, Blend no sabe cómo crear datos de muestra de ese tipo.

Con respecto a la pregunta implícita de cómo obtener soporte de tiempo de diseño: hay algunas formas de lograr datos de tiempo de diseño en WPF, pero mi método preferido en este momento para escenarios complejos es crear un DataSourceProvider personalizado. Para dar crédito donde se debe: obtuve la idea de este artículo (que es aún más antiguo que esta pregunta).

La solución DataSourceProvider

Cree una clase que implemente DataSourceProvider y devuelva una muestra de su contexto de datos. Pasar el MainWindowViewModel instanciado al método OnQueryFinished es lo que hace que ocurra la magia (sugiero leer sobre esto para entender cómo funciona).

internal class SampleMainWindowViewModelDataProvider : DataSourceProvider { private MainWindowViewModel GenerateSampleData() { var myViewModel1 = new MyViewModel { EventName = "SampleName1" }; var myViewModel2 = new MyViewModel { EventName = "SampleName2" }; var myViewModelCollection1 = new ObservableCollection<MyViewModel> { myViewModel1, myViewModel2 }; var timeToMyViewModelDictionary = new Dictionary<DateTime, ObservableCollection<MyViewModel>> { { DateTime.Now, myViewModelCollection1 } }; var viewModel = new MainWindowViewModel() { TimesAndEvents = timeToMyViewModelDictionary }; return viewModel; } protected sealed override void BeginQuery() { OnQueryFinished(GenerateSampleData()); } }

Todo lo que tiene que hacer ahora es agregar su proveedor de datos como un contexto de datos de muestra en su vista:

<Window x:Class="SampleDataInBlend.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:SampleDataInBlend" mc:Ignorable="d" Title="MainWindow" Height="200" Width="300"> <d:Window.DataContext> <local:SampleMainWindowViewModelDataProvider/> </d:Window.DataContext> <Grid> <ListBox ItemsSource="{Binding TimesAndEvents}"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Key}"/> <ListBox ItemsSource="{Binding Value}"> <ListBox.ItemTemplate> <DataTemplate DataType="{x:Type local:MyViewModel}"> <TextBlock Text="{Binding EventName}"/> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> </Window>

Nota: la ''d'' en <d:Window.DataContext> es importante ya que le dice a Blend y al compilador que ese elemento específico es para el tiempo de diseño y debe ignorarse cuando se compila el archivo.

Después de hacer eso, mi vista de diseño ahora se parece a lo siguiente:

Configurando el problema

Comencé con 5 clases (2 se generaron a partir de la plantilla de proyecto de WPF, que recomiendo usar para esto):

  1. MyViewModel.cs
  2. MainWindowViewModel.cs
  3. MainWindow.xaml
  4. App.xaml

MyViewModel.cs

public class MyViewModel { public string EventName { get; set; } }

MainWindowViewModel.cs

public class MainWindowViewModel { public IDictionary<DateTime, ObservableCollection<MyViewModel>> TimesAndEvents { get; set; } = new Dictionary<DateTime, ObservableCollection<MyViewModel>>(); public void Initialize() { //Does some service call to set the TimesAndEvents property } }

MainWindow.cs

Tomé la clase MainWindow generada y la cambié. Básicamente, ahora solicita un MainWindowViewModel y lo establece como su DataContext.

public partial class MainWindow : Window { public MainWindow(MainWindowViewModel viewModel) { DataContext = viewModel; InitializeComponent(); } }

MainWindow.xaml

Tenga en cuenta la falta de contexto de datos de diseño de la Solución.

<Window x:Class="SampleDataInBlend.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:SampleDataInBlend" mc:Ignorable="d" Title="MainWindow" Height="200" Width="300"> <Grid> <ListBox ItemsSource="{Binding TimesAndEvents}"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Key}"/> <ListBox ItemsSource="{Binding Value}"> <ListBox.ItemTemplate> <DataTemplate DataType="{x:Type local:MyViewModel}"> <TextBlock Text="{Binding EventName}"/> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> </Window>

App.cs

En primer lugar, elimine StartupUri="MainWindow.xaml" del lado xaml ya que lanzaremos MainWindow desde el código que está detrás.

public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); var viewModel = new MainWindowViewModel(); // MainWindowViewModel needs to have its dictionary filled before its // bound to as the IDictionary implementation we are using does not do // change notification. That is why were are calling Initialize before // passing in the ViewModel. viewModel.Initialize(); var view = new MainWindow(viewModel); view.Show(); } }

Construir y ejecutar

Ahora, si todo se hizo correctamente y completó el método de Inicialización de MainWindowViewModel (incluiré mi implementación en la parte inferior), debería ver una pantalla como la que se muestra a continuación cuando compile y ejecute su aplicación WPF:

¿Cuál fue el problema otra vez?

El problema era que nada se mostraba en la vista de diseño.

Mi método Initialize ()

public void Initialize() { TimesAndEvents = PretendImAServiceThatGetsDataForMainWindowViewModel(); } private IDictionary<DateTime, ObservableCollection<MyViewModel>> PretendImAServiceThatGetsDataForMainWindowViewModel() { var myViewModel1 = new MyViewModel { EventName = "I''m real" }; var myViewModel2 = new MyViewModel { EventName = "I''m real" }; var myViewModelCollection1 = new ObservableCollection<MyViewModel> { myViewModel1, myViewModel2 }; var timeToMyViewModelDictionary = new Dictionary<DateTime, ObservableCollection<MyViewModel>> { { DateTime.Now, myViewModelCollection1 } }; return timeToMyViewModelDictionary; }