poniedziałek, 28 listopada 2011

State-Based Navigation w Prism - powiadamianie o stanie aplikacji

W Prism mamy do dyspozycji dwa kluczowe sposoby poruszania się po aplikacji: State-Based Navigation oraz View Based Navigation. W tym artykule omówię pierwszy z tych sposobów.

Base-State Navigation odnosi się do zmiany stanu kontrolek na formatce czyli np. ukrycie kontrolki, pokazanie, dodawanie animacji itp. Nawigacja ta występuje w obrębie pojedynczego widoku.
Obsługa tego typu nawigacji może być spowodowana różnymi rodzaju zachowaniami np.:
  • zmiana wartości właściwości w ViewModelu
  • interakcja użytkownika z aplikacją (kliknięcie na kontrolkę itp.)
  • zmiana sposobu prezentowania danych użytkownika
Kiedy nie używać Base-State Navigation:
  • kiedy chcemy wyświetlać dane z różnych źródeł 
  • użytkownik chce wykonywać zadania nie związane z tym co aktualnie widzi na ekranie
  • skomplikowane zmiany wyglądu
Po tym krótkim wstępie czas na jakieś przykładowe aplikacje, które pokażą w praktyce o co chodzi w opisanej powyżej nawigacji.

1. Powiadamianie o stanie aplikacji.
W tym przykładzie pokażemy użytkownikowi co dzieje się podczas działania naszej aplikacji.

Na początek stwórzmy nasz projekt który będzie mieć następującą postać:



Mając ten szablon przyda się nam kod typów tutaj występujących:

Customer.cs:


Code:
namespace BusinessObjects
{
    public class Customer
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string CompanyName { get; set; }
        public string EmailAddress { get; set; }
        public string Phone { get; set; }
    }
}

IRepository.cs:


Code:
namespace Repositories
{
    public interface IRepository
    {
    }
}

ICustomerRepository.cs:


Code:
using System.Collections.Generic;
using BusinessObjects;

namespace Repositories.Customers
{
    public interface ICustomerRepository : IRepository
    {
        List<Customer> GetAllCustomers();
    }
}


CustomerRepository.cs:


Code:
using System.Collections.Generic;
using System.Data.SqlClient;
using BusinessObjects;
using Repositories.Customers;

namespace MSSQLRepository.Customers
{
    public class CustomerRepository : ICustomerRepository
    {
        public List<Customer> GetAllCustomers()
        {
            var customerList = new List<Customer>();
            using (var conn = new SqlConnection(Configuration.ConnectionString))
            {
                using(var command = conn.CreateCommand())
                {
                    command.CommandText = "SELECT CustomerID, FirstName, LastName, CompanyName, EmailAddress, Phone " +
                                          "FROM SalesLT.Customer";
                    conn.Open();
                    using(var dr = command.ExecuteReader())
                    {
                        while(dr.Read())
                        {
                            customerList.Add(CustomerMapper(dr));
                        }
                        dr.Close();
                    }
                }
            }

            return customerList;
        }

        private static Customer CustomerMapper(SqlDataReader dr)
        {
            var customer = new Customer
            {Id = int.Parse(dr["CustomerID"].ToString()), FirstName = (string) dr["FirstName"]};
            customer.FirstName = (string)dr["LastName"];
            customer.CompanyName = (string)dr["CompanyName"];
            customer.EmailAddress = (string)dr["EmailAddress"];
            customer.Phone = (string)dr["Phone"];

            return customer;
        }
    }
}

Configuration.cs:


Code:
namespace MSSQLRepository
{
    public class Configuration
    {
        public static string ConnectionString =
            "Data Source=localhost;Initial Catalog=AdventureWorksLT2008R2;Integrated Security=SSPI;";
    }
}

BaseViewModel.cs:


Code:
using System.ComponentModel;

namespace Infrastructure
{
    public class BaseViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        public void RaisePropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

RegionNames.cs:


Code:
namespace Infrastructure
{
    public static class RegionNames
    {
        public static string MainContentRegion = "MainContentRegion";
    }
}

Bootstrapper.cs:


Code:
using System.Windows;
using MSSQLRepository.Customers;
using Microsoft.Practices.Prism.Modularity;
using Microsoft.Practices.Prism.UnityExtensions;
using Microsoft.Practices.Unity;
using Repositories.Customers;

namespace Sample1
{
    public class Bootstrapper : UnityBootstrapper
    {
        protected override void ConfigureContainer()
        {
            base.ConfigureContainer();
            Container.RegisterType(typeof (ICustomerRepository), typeof (CustomerRepository), new ContainerControlledLifetimeManager());
        }

        protected override IModuleCatalog CreateModuleCatalog()
        {
            var moduleCatalog = base.CreateModuleCatalog();
            var customerModule = typeof (CustomerModuleCustomerModule);
            moduleCatalog.AddModule(new ModuleInfo(customerModule.Name, customerModule.AssemblyQualifiedName));

            return moduleCatalog;
        }

        protected override void InitializeShell()
        {
            Application.Current.MainWindow = (Shell)Shell;
            Application.Current.MainWindow.Show();
        }

        protected override DependencyObject CreateShell()
        {
            return Container.Resolve<Shell>();
        }
    }
}

Shell.xaml.cs:


Code:
<Window x:Class="Sample1.Shell"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:prism="clr-namespace:Microsoft.Practices.Prism.Regions;assembly=Microsoft.Practices.Prism"
        xmlns:Infrastructure="clr-namespace:Infrastructure;assembly=Infrastructure" Title="Shell" Height="300" Width="300">
    <Grid>
        <ContentControl prism:RegionManager.RegionName="{x:Static Infrastructure:RegionNames.MainContentRegion}" />
    </Grid>
</Window>

Czas na nasz moduł - CustomerModule:


Code:
<UserControl x:Class="CustomerModule.CustomerListView"
             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" >
    <Grid>
        <DataGrid ItemsSource="{Binding CustomerList, Mode=TwoWay}" />
    </Grid>
</UserControl>

CustomerListViewModel.cs:


Code:
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using BusinessObjects;
using Infrastructure;
using Repositories.Customers;

namespace CustomerModule
{
    public class CustomerListViewModel : BaseViewModel
    {
        private readonly ICustomerRepository _customerRepository;

        private List<Customer> _customerList; 
        public List<Customer> CustomerList
        {
            get
            {
                if(_customerList != null)
                {
                    return _customerList;
                }
                var loadCustomerTask = new Task(() =>
                                                    {
                                                        Thread.Sleep(5000);
                                                        CustomerList = _customerRepository.GetAllCustomers();
                                                    });
                
                loadCustomerTask.Start();
                return _customerList;
            }
            set
            {
                _customerList = value;
                RaisePropertyChanged("CustomerList");
            }
        }

        public CustomerListViewModel(ICustomerRepository customerRepository)
        {
            _customerRepository = customerRepository;
        }
    }
}


Code:
using Infrastructure;
using Microsoft.Practices.Prism.Modularity;
using Microsoft.Practices.Prism.Regions;

namespace CustomerModule
{
    public class CustomerModule : IModule
    {
        private readonly IRegionManager _regionManager;

        public CustomerModule(IRegionManager regionManager)
        {
            _regionManager = regionManager;
        }

        public void Initialize()
        {
            _regionManager.RegisterViewWithRegion(RegionNames.MainContentRegion, typeof (CustomerListView));
        }
    }
}

Dla przypomnienia sposób powyższy to View Discovery - czyli widok zostanie załadowany zaraz po uruchomieniu naszej aplikacji.
Opiszmy naszą aplikację - mamy do czynienia z bazą (AdventureWorks) z której są pobierane dane i ładowane do kontrolki Grid. Symulujemy tutaj 5 sekundowe opóźnienie podczas ładowania danych. Co w tym momencie widzi użytkownik? Właściwie nic. Będzie musiał poczekać 5 sekund. Nawet jeśli poczeka (nie wyłączy wcześniej aplikacji) to jednak i tak nie wie do końca co dzieje się w tym momencie.
Należy więc poinformować użytkownika co dzieje się w danym momencie z naszą aplikacją.
W tym celu skorzystamy z kontrolki BusyIndicator dostępnej w pakiecie Extended WPF Toolkit.
Do naszego kodu XAML dokładamy następujące linijki:


Code:
<UserControl x:Class="CustomerModule.CustomerListView"
             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:extToolkit="http://schemas.microsoft.com/winfx/2006/xaml/presentation/toolkit/extended"
             mc:Ignorable="d" >
    <Grid>
        <extToolkit:BusyIndicator IsBusy="{Binding IsBusy}" BusyContent="Loading data...">
            <DataGrid ItemsSource="{Binding CustomerList, Mode=TwoWay}" />
        </extToolkit:BusyIndicator>
    </Grid>
</UserControl>

Do ViewModelu:


Code:
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using BusinessObjects;
using Infrastructure;
using Repositories.Customers;

namespace CustomerModule
{
    public class CustomerListViewModel : BaseViewModel
    {
        private readonly ICustomerRepository _customerRepository;

        private List<Customer> _customerList; 
        public List<Customer> CustomerList
        {
            get
            {
                IsBusy = true;
                if(_customerList != null)
                {
                    IsBusy = false;
                    return _customerList;
                }
                var loadCustomerTask = new Task(() =>
                                                    {
                                                        Thread.Sleep(5000);
                                                        CustomerList = _customerRepository.GetAllCustomers();
                                                        IsBusy = false;
                                                    });
                
                loadCustomerTask.Start();
                return _customerList;
            }
            set
            {
                _customerList = value;
                RaisePropertyChanged("CustomerList");
            }
        }

        private bool _isBusy;
        public bool IsBusy
        {
            get { return _isBusy; }
            set { _isBusy = value;
            RaisePropertyChanged("IsBusy");
            }
        }

        public CustomerListViewModel(ICustomerRepository customerRepository)
        {
            _customerRepository = customerRepository;
        }
    }
}

Użytkownik po uruchomieniu programu otrzyma następujący rezultat:


Jest to znacznie przyjemniejszy widok, niż czekanie przed pustym ekranem - bez wiedzy czy aplikacja jeszcze działa i odpowiada na nasze komendy.

W następnym poście pokaże w jaki sposób za pomocą stylów uzyskać różne widoki naszej aplikacji Prism.

Brak komentarzy:

Prześlij komentarz