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.