Tutorial MVVM VII

Tutorial MVVM VII

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&lt;Location&gt; _locations;
        public List&lt;Location&gt; 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 =&gt; GetSearch())); }
        }

        private void GetSearch()
        {
            if (String.IsNullOrEmpty(TextSearch)) return;

            if (TextSearch.Length &gt;= 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.

Resources

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 &amp;&amp; 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.