Continuando con el Tutorial de MVVM, en este post continuamos con el ViewModel, después de generar la clase abstracta para la notificación de cambios, y la clase que implementa ICommand, empezamos a crear propiedades en la vista-modelo del componente Search.
Creamos cinco propiedades públicas y un método privado. Las propiedades son, un String que se enlazará con el TextBox de la vista, un ICommand que se enlazará con el botón de búsqueda, una propiedad List<Model.Location>, que es la fuente de datos de la que se va a alimentar el ListBox, otra que será la localización que se seleccione en el ListBox. Y una propiedad tipo ViewModel.MainWindow que será el contexto de datos del MainWindow, de esta forma podremos llamar a un método en esa vista-modelo. El método hará una llamada a la clase Client dónde habíamos definido una petición al WebService de Bing Maps.
ViewModel.Search.cs
using System; using System.Collections.Generic; using System.Windows.Input; using WeatherApp.Common; using WeatherApp.Model; namespace WeatherApp.ViewModel { public class Search : ObservableObject { private const int MinLength = 3; public MainWindow Mw { get; set; } private String _textSearch; public String TextSearch { get { return _textSearch; } set { _textSearch = value; OnPropertyChanged("TextSearch"); } } private List<Location> _locations; public List<Location> Locations { get { return _locations; } set { _locations = value; OnPropertyChanged("Locations"); } } private Location _location; public Location Location { get { return _location; } set { _location = value; OnPropertyChanged("Location"); Mw.GetWeather(value); } } private ICommand _searchIt; public ICommand SearchIt { get { return _searchIt ?? (_searchIt = new RelayCommand(param => GetSearch())); } } private void GetSearch() { if (String.IsNullOrEmpty(TextSearch)) return; if (TextSearch.Length >= MinLength) { Locations = Client.GetLocations(TextSearch); } } } }
A continuación enlazamos las propiedades que hemos definido a la vista, es decir al componente Search, para enlazar estas propiedades, hay que ir a cada elemento del XAML y establecer un Binding.
Search.xaml
<UserControl x:Class="WeatherApp.Components.Search" 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" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition /> </Grid.RowDefinitions> <StackPanel Grid.Row="0" Orientation="Vertical" Margin="5"> <TextBox MinWidth="200" Text="{Binding Path=TextSearch}"/> <Button Content="Buscar" HorizontalAlignment="Right" Command="{Binding Path=SearchIt}"/> </StackPanel> <ListBox Grid.Row="1" Margin="5" ItemsSource="{Binding Path=Locations}" ItemTemplate="{DynamicResource DtLocations}" SelectedValue="{Binding Path=Location}"> <ListBox.Resources> <DataTemplate x:Key="DtLocations"> <Grid MaxWidth="250"> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <StackPanel Grid.Row="1" Orientation="Horizontal"> <TextBlock Margin="3"> <Run Text="Lat: " FontWeight="Bold"/> <Run Text="{Binding Path=Coordinates.Latitude, StringFormat=N2}"/> </TextBlock> <TextBlock Margin="3"> <Run Text="Lon: " FontWeight="Bold"/> <Run Text="{Binding Path=Coordinates.Longitude, StringFormat=N2}"/> </TextBlock> </StackPanel> <TextBlock Grid.Row="0" FontWeight="Bold" FontSize="14pt"> <TextBlock.Text> <MultiBinding StringFormat="{}{0} ({1})"> <Binding Path="Name"/> <Binding Path="Country"/> </MultiBinding> </TextBlock.Text> </TextBlock> </Grid> </DataTemplate> </ListBox.Resources> </ListBox> </Grid> </UserControl>
Seguimos con la página Weather que en su ViewModel tiene cuatro propiedades y un método, una propiedad que define la ruta de la imagen que se va a cargar con todos los datos del clima de la ciudad seleccionada, una de tipo Weather, otra tipo DataPoint con el clima actual, y una última que indica si es Farenheit o Celsius. El método transforma el clima en una imagen que he cargado en la carpeta Resources, y a las que le he cambiado la forma en que se incluyen en la compilación a Contenido y que se copie siempre.
ViewModel.Weather.cs
using System; using System.IO; using WeatherApp.Common; using WeatherApp.Model; namespace WeatherApp.ViewModel { public class Weather : ObservableObject { private Uri _imageSource; public Uri ImageSource { get { return _imageSource; } set { _imageSource = value; OnPropertyChanged("ImageSource"); } } private Model.Weather _city; public Model.Weather City { get { return _city; } set { _city = value; OnPropertyChanged("City"); GetCurrent(); } } private bool _isFarenheit; public bool IsFarenheit { get { return _isFarenheit; } set { _isFarenheit = value; OnPropertyChanged("IsFarenheit"); } } private DataPoint _currentWeather; public DataPoint CurrentWeather { get { return _currentWeather; } set { _currentWeather = value; OnPropertyChanged("CurrentWeather"); } } private void GetCurrent() { if (City != null && City.currently != null) { CurrentWeather = City.currently; var png = String.Format(@"{0}Resources{1}.png", Environment.CurrentDirectory, CurrentWeather.icon); ImageSource = new FileInfo(png).Exists ? ImageSource = new Uri(png, UriKind.RelativeOrAbsolute) : null; } else { ImageSource = null; } } } }
Y enlazamos las propiedades a la página.
Weather.xaml
<Page x:Class="WeatherApp.Components.Weather" 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" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300" Title="Weather"> <Grid Background="Black"> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <Image Grid.Column="0" Stretch="Uniform" VerticalAlignment="Center" Source="{Binding Path=ImageSource}"/> <StackPanel Grid.Column="1" VerticalAlignment="Center" Orientation="Vertical" Margin="0,0,30,0"> <TextBlock FontSize="36pt" FontWeight="Bold" Text="{Binding Path=CurrentWeather.temperature}" Foreground="White"/> <TextBlock FontSize="18pt" FontWeight="Bold" Text="{Binding Path=CurrentWeather.icon}" Foreground="White"/> </StackPanel> <Border Grid.ColumnSpan="2" CornerRadius="5" VerticalAlignment="Top" HorizontalAlignment="Right" Margin="0,5,30,0"> <StackPanel Orientation="Horizontal"> <RadioButton GroupName="Degree" Foreground="White">Fº</RadioButton> <RadioButton GroupName="Degree" Foreground="White" IsChecked="{Binding Path=IsFarenheit}">Cº</RadioButton> </StackPanel> </Border> </Grid> </Page>
Y seguimos con la ViewModel de MainWindow, aquí estableceremos tres propiedades y dos métodos públicos. La primera propiedad indica si está o no expandido el Expander, la segunda es de tipo ViewModel.Search y una tercera que es un objeto Frame. Los dos métodos son uno que inicia el componente de búsqueda, y el segundo es un método que obtiene el clima de la ciudad seleccionada.
ViewModel.MainWindow.cs
using System.Windows.Controls; using WeatherApp.Common; using WeatherApp.Model; namespace WeatherApp.ViewModel { public class MainWindow : ObservableObject { private bool _isSearchExpanded; public bool IsSearchExpanded { get { return _isSearchExpanded; } set { _isSearchExpanded = value; OnPropertyChanged("IsSearchExpanded"); } } public Search Search { get; set; } public Frame Browser { get; set; } public void Init() { Search = new Search { Mw = this }; } public void GetWeather(Location location) { if (location != null) { var city = Client.GetWeather(location.Coordinates); var vm = new Weather { City = city, IsFarenheit = false }; Browser.Navigate(new Components.Weather { DataContext = vm }); IsSearchExpanded = false; } } } }
MainWindow.xaml
<Window x:Class="WeatherApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:comp="clr-namespace:WeatherApp.Components" Title="MainWindow" Height="350" Width="525"> <Grid> <Frame x:Name="Browser"/> <Expander ExpandDirection="Left" IsExpanded="{Binding Path=IsSearchExpanded}" HorizontalAlignment="Right" Background="Gainsboro"> <Expander.Header> <TextBlock Text="Búsqueda"> <TextBlock.LayoutTransform> <TransformGroup> <ScaleTransform/> <SkewTransform/> <RotateTransform Angle="90"/> <TranslateTransform/> </TransformGroup> </TextBlock.LayoutTransform> </TextBlock> </Expander.Header> <comp:Search DataContext="{Binding Path=Search}"/> </Expander> </Grid> </Window>
Y por último, para poder enlazar los distintos ViewModel a los componentes, tenemos que enlazar el ViewModel.MainWindow.cs al contexto de datos de MainWindow, y para ello accedemos al evento Initialize() de MainWindow, y establecemos el enlace al contexto.
MainWindow.xaml.cs
using System.Windows; namespace WeatherApp { /// <summary>Interaction logic for MainWindow.xaml</summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); var vm = new ViewModel.MainWindow {Browser = Browser}; vm.Init(); DataContext = vm; } } }
Nota: Aunque es una práctica habitual acceder al code behind para incluir los DataContext, no es la más correcta, que sería a través de una clase que gestionase los distintos contextos, y que fuese inicializada en un sólo lugar de nuestra aplicación.
En el siguiente post, el uso de converters, en el que cambiaremos los grados que actualmente estamos mostrando como Farenheit.