В этой статье мы
поговорим о том, как создать простой Wizard на WPF. Когда мы долгое время пишем на WPF, может возникнуть задача написания простого
визарда, чтобы, например, задать настройки в программе, инициировать некоторые
данные и т.д. Первым делом, когда нужно что-то реализовать, мы посмотрим, как
это сделали до нас, чтобы основываться на чужом опыте. Одно из интересных
решений для создания визарда предлагает Josh
Smith, Creating an Internationalized Wizard in WPF. Этот автор является
гуру по созданию WPF-приложений, и
если вы до сих пор не сталкивались с его творчеством, рекомендую почитать его блог. Если вы увлекаетесь
разработкой WPF-приложений, то вам должен понравиться этот блог.
Я продемонстрирую свой
вариант создания простого визарда, без каких-либо заморочек, с детальным пошаговым разбором. Для того чтобы сделать визард, необходимо
реализовать главное окно, в котором будет показана наша информация, а все ViewModel, которые
будут привязанными к конкретным View, будут отображаться через ControlPresenter. Для начала создадим
простое WPF-приложение, которое назовем SimpleWizard.
Будучи приверженцем паттерна MVVM, предпочту реализацию визарда с использованием данного паттерна. Поэтому если вы не
знакомы с использованием данного паттерна в разработке приложений на WPF, рекомендую
посмотреть статьи "Основы паттерна MVVM"
и "MVVM Part 2". В этой статье не будет рассказываться о сведениях про MVVM
и о его использовании; здесь будет просто показано его практическое применение. Вот
какой внешний вид будет у нашего визарда:
Исходный код главного
окна приведен ниже.
<Window x:Class="SimpleWizard.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="{Binding Title}"
Width="640"
Height="520"
ResizeMode="CanMinimize"
WindowStartupLocation="CenterScreen"
Background="{DynamicResource WindowBackgroundBrush}"
UseLayoutRounding="True"
>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="9"/>
<ColumnDefinition Width="615"/>
<ColumnDefinition Width="10"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="9"/>
<RowDefinition Height="428"/>
<RowDefinition Height="52"/>
<RowDefinition Height="5"/>
</Grid.RowDefinitions>
<ScrollViewer
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Grid.Row="1" Grid.Column="1">
<ContentPresenter Content="{Binding CurrentPage}" DataContext="{Binding CurrentPage}"/>
</ScrollViewer>
<Separator
Height="17"
Margin="0,0,0,35"
Grid.Row="2"
VerticalAlignment="Bottom"
Grid.ColumnSpan="3"/>
<Button Content="Back"
Margin="0,0,183,13"
VerticalAlignment="Bottom"
FontSize="12"
HorizontalAlignment="Right"
Width="75"
Grid.Row="2"
Grid.Column="1"
Command="{Binding MovePreviousCommand}"
/>
<Button
Margin="0,0,103,13"
VerticalAlignment="Bottom"
FontSize="12"
Grid.Row="2"
Grid.Column="1"
HorizontalAlignment="Right"
Width="75"
Command="{Binding MoveNextCommand}"
Style="{StaticResource moveNextButtonStyle}"
/>
<Button Content="Cancel"
Margin="0,0,23,13"
VerticalAlignment="Bottom"
FontSize="12"
Grid.Row="2"
Grid.Column="1"
HorizontalAlignment="Right"
Width="75"
Command="{Binding CancelCommand}" />
</Grid>
</Window>
Первоначальный вид
структуры проекта приведена ниже.
В папке Assets мы
будем размещать стили, ресурсы и т.д. Пока там просто добавлен стиль внешнего
вида визарда (цвет визарда).
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="WinBackgroundBrush" Color="#FFF0F0F0"/>
<SolidColorBrush x:Key="WindowBackgroundBrush" Color="#FFF0F0F0"/>
</ResourceDictionary>
Привязка данного файла
ресурсов выполнена через App.xaml,
реализацию которого мы приведем ниже. Поскольку мы увидели внешний вид визарда,
осталось добавить нужные нам страницы, которые будут показаны в визарде.
Теперь добавим реализацию
трех страниц и создадим для них соответствующие ViewModel.
Реализация первой
страницы Page1:
<UserControl x:Class="SimpleWizard.View.Page1"
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>
<ListView x:Name ="library" ItemsSource="{Binding Books}" SelectedItem="{Binding SelectedBook}">
<ListView.ItemTemplate>
<DataTemplate>
<WrapPanel>
<TextBlock Text="Author: " />
<TextBlock Text="{Binding Author}" FontWeight="Bold" />
<TextBlock Text=", " />
<TextBlock Text="Caption: " />
<TextBlock Text="{Binding Title}" FontWeight="Bold" />
<TextBlock Text="Count: " />
<TextBlock Text="{Binding Count}" FontWeight="Bold" />
</WrapPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</UserControl>
На данной странице я
взял реализацию электронной библиотеки, которою реализовывал ранее.
Реализация второй
страницы Page2:
<UserControl x:Class="SimpleWizard.View.Page2"
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>
<StackPanel>
<TextBlock Text="Cup of tea"></TextBlock>
<Button Content="Add new tea"></Button>
</StackPanel>
</Grid>
</UserControl>
Реализация третей
страницы Page3:
<UserControl x:Class="SimpleWizard.View.Page3"
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>
<StackPanel>
<RadioButton Content="Red" Background="Red"></RadioButton>
<RadioButton Content="Green" Background="Green"></RadioButton>
<RadioButton Content="Blue" Background="Blue"></RadioButton>
</StackPanel>
</Grid>
</UserControl>
Реализация главной
формы приведена ниже. Вы можете ее изменить сразу по желанию, поскольку ее реализация очень простая. Стили, которые приведены в данной форме, будут
реализованы позже. Страницы визарда будут выведены через ControlPresenter. Код главной страницы
приведен ниже.
<Window x:Class="SimpleWizard.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="{Binding Title}"
Width="640"
Height="520"
ResizeMode="CanMinimize"
WindowStartupLocation="CenterScreen"
Background="{DynamicResource WindowBackgroundBrush}"
UseLayoutRounding="True"
>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="9"/>
<ColumnDefinition Width="615"/>
<ColumnDefinition Width="10"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="9"/>
<RowDefinition Height="428"/>
<RowDefinition Height="52"/>
<RowDefinition Height="5"/>
</Grid.RowDefinitions>
<ScrollViewer
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Grid.Row="1" Grid.Column="1">
<ContentPresenter Content="{Binding CurrentPage}" DataContext="{Binding CurrentPage}"/>
</ScrollViewer>
<Separator
Height="17"
Margin="0,0,0,35"
Grid.Row="2"
VerticalAlignment="Bottom"
Grid.ColumnSpan="3"/>
<Button Content="Back"
Margin="0,0,183,13"
VerticalAlignment="Bottom"
FontSize="12"
HorizontalAlignment="Right"
Width="75"
Grid.Row="2"
Grid.Column="1"
Command="{Binding MovePreviousCommand}"
/>
<Button
Margin="0,0,103,13"
VerticalAlignment="Bottom"
FontSize="12"
Grid.Row="2"
Grid.Column="1"
HorizontalAlignment="Right"
Width="75"
Command="{Binding MoveNextCommand}"
Style="{StaticResource moveNextButtonStyle}"
/>
<Button Content="Cancel"
Margin="0,0,23,13"
VerticalAlignment="Bottom"
FontSize="12"
Grid.Row="2"
Grid.Column="1"
HorizontalAlignment="Right"
Width="75"
Command="{Binding CancelCommand}" />
</Grid>
</Window>
Теперь для этих страниц
необходимо создать ViewModel,
в
которой будет реализована необходимая логика. Для этого создадим базовую
модель, от которой будем наследоваться, для создания необходимых ViewModel.
public abstract class WizardBaseViewModel : NotifyModelBase
{
public abstract string Title { get; }
public abstract bool IsValid();
bool _isCurrentPage;
public bool IsCurrentPage
{
get { return _isCurrentPage; }
set
{
if (value == _isCurrentPage)
return;
_isCurrentPage = value;
OnPropertyChanged("IsCurrentPage");
}
}
}
Выше приведен базовый класс
WizardBaseViewModel,
который
и будет отвечать за логику страниц в визарде. Более сложные программные системы, которые используют паттерн MVVM,
в
большинстве построены на наследовании, а не на композиции. Модель NotifyModelBase
представляет
собой класс, который реализует интерфейс INotifyPropertyChanged. Этот класс
необходим, чтобы уведомить UI
об
изменении. Ниже приведена реализация этого класса.
public class NotifyModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Модель
первой страницы я назвал LibaryViewModel, так как, по сути, только в
этой модели представления для примера реализована связь с моделью и вывод
данных в представление.
public class LibraryViewModel : WizardBaseViewModel
{
#region
Constructor
public LibraryViewModel()
{
_books = new ObservableCollection<IBook>();
_books.Add(new LibraryBook { Author = "Jon
Skeet",
Title = "C#
in Depth",
Count = 3, SN = "ISBN: 9781617291340", Year = new DateTime(2013, 9, 10) });
_books.Add(new LibraryBook { Author = "Martin
Fowler",
Title = "Refactoring:
Improving the Design of Existing Code", Count = 2, SN = "ISBN-10:
0201485672", Year = new DateTime(1999, 7, 8) });
_books.Add(new LibraryBook { Author = "Jeffrey
Richter",
Title = "CLR
via C# (Developer Reference)", Count = 5, SN = "ISBN-10: 0735667454", Year = new DateTime(2012, 12, 4) });
}
#endregion
private ObservableCollection<IBook> _books;
public ObservableCollection<IBook> Books
{
get { return _books; }
set
{
_books = value;
OnPropertyChanged("Books");
}
}
public override string Title
{
get
{
return "First Page";
}
}
public override bool IsValid()
{
return true;
}
}
Модель,
которая связывается с представлением, выглядит следующим образом:
public interface IBook
{
string Author { get; set; }
string Title { get; set; }
DateTime Year { get; set; }
string SN { get; set; }
int Count { get; set; }
}
public class LibraryBook : NotifyModelBase, IBook
{
private string _author;
public string Author
{
get { return _author; }
set
{
_author = value;
OnPropertyChanged("Author");
}
}
private string _title;
public string Title
{
get { return _title; }
set
{
_title = value;
OnPropertyChanged("Title");
}
}
private DateTime _year;
public DateTime Year
{
get { return _year; }
set
{
_year = value;
OnPropertyChanged("Year");
}
}
private string _sn;
public string SN
{
get { return _sn; }
set
{
_sn = value;
OnPropertyChanged("SN");
}
}
private int _count;
public int Count
{
get { return _count; }
set
{
_count = value;
OnPropertyChanged("Count");
}
}
}
Осталось
привести модель представления для второй страницы визарда.
public class PageTwoViewModel : WizardBaseViewModel
{
public override string Title
{
get { return "Page 2"; }
}
public override bool IsValid()
{
return true;
}
}
И для
третьей страницы приведем модель представления.
public class PageThreeViewModel : WizardBaseViewModel
{
public override string Title
{
get { return "Page 3"; }
}
public override bool IsValid()
{
return true;
}
}
Чтобы
сделать привязку модели представления с конкретным представлением, воспользуемся
обычным словарем ресурсов в WPF.
Ниже представлена
реализация связывания с помощью DataTemplate.
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ViewModel="clr-namespace:SimpleWizard.ViewModel"
xmlns:View="clr-namespace:SimpleWizard.View">
<DataTemplate DataType="{x:Type ViewModel:LibraryViewModel}">
<View:Page1/>
</DataTemplate>
<DataTemplate DataType="{x:Type ViewModel:PageTwoViewModel}">
<View:Page2 />
</DataTemplate>
<DataTemplate DataType="{x:Type ViewModel:PageThreeViewModel}">
<View:Page3 />
</DataTemplate>
<Style TargetType="{x:Type Button}" x:Key="moveNextButtonStyle">
<Setter Property="Content" Value="Next" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsOnLastPage}" Value="True">
<Setter Property="Content" Value="Finish" />
</DataTrigger>
</Style.Triggers>
</Style>
<SolidColorBrush x:Key="WinBackgroundBrush" Color="#FFF0F0F0"/>
<SolidColorBrush x:Key="WindowBackgroundBrush" Color="#FFF0F0F0"/>
</ResourceDictionary>
Осталось сделать
несколько штрихов. Первым делом сделаем доступным данный словарь для
всего приложения. Для этого перейдем в файл App.xaml и
изменим эго следующим образом:
<Application x:Class="SimpleWizard.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
>
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Assets/Resource.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
Изменения небольшие. Если
вы сравните свой файл с тем, который изменил я, то увидите, что, по сути, убралась
строчка StartupUri и добавилась строчка видимости для
словаря. Поскольку мы убрали StartupUri, нам необходимо перейти в файл App.xaml.cs и
переопределить метод OnStartup.
Но перед этим нужно реализовать главную ViewModel, которая
будет отвечать за переключение между страницами визарда. Это модель
представления, которая будет исполнять роль контролера в модели MVC, но для паттерна MVVM.
public class MainViewModel : NotifyModelBase
{
public MainViewModel()
{
CurrentPage = Pages[0];
}
/// <summary>
/// Returns the command which,
when executed, cancels the order
/// and causes the Wizard to be
removed from the user interface.
/// </summary>
private DelegateCommand _cancelCommand;
public DelegateCommand CancelCommand
{
get
{
if (_cancelCommand == null)
_cancelCommand = new DelegateCommand(arg => CancelOrder());
return _cancelCommand;
}
}
void CancelOrder()
{
OnRequestClose();
}
private DelegateCommand _moveNextCommand;
public DelegateCommand MoveNextCommand
{
get
{
return _moveNextCommand ??
(_moveNextCommand = new DelegateCommand(
arg => MoveToNextPage(),
arg => CanMoveToNextPage));
}
}
bool CanMoveToNextPage
{
get { return CurrentPage != null && CurrentPage.IsValid();
}
}
void MoveToNextPage()
{
if (CanMoveToNextPage)
{
if (CurrentPageIndex <
Pages.Count - 1)
CurrentPage =
Pages[CurrentPageIndex + 1];
else
OnRequestClose();
}
}
#region MovePreviousCommand
/// <summary>
/// Returns the command which,
when executed, causes the CurrentPage
/// property to reference the
previous page in the workflow.
/// </summary>
private DelegateCommand _movePreviousCommand;
public DelegateCommand MovePreviousCommand
{
get
{
return _movePreviousCommand ??
(_movePreviousCommand = new DelegateCommand(
args => MoveToPreviousPage(),
args => CanMoveToPreviousPage));
}
}
bool CanMoveToPreviousPage
{
get { return 0 < CurrentPageIndex; }
}
void MoveToPreviousPage()
{
if (CanMoveToPreviousPage)
CurrentPage =
Pages[CurrentPageIndex - 1];
}
#endregion //
MovePreviousCommand
/// <summary>
/// Returns the page ViewModel
that the user is currently viewing.
/// </summary>
private WizardBaseViewModel _currentPage;
public WizardBaseViewModel CurrentPage
{
get { return _currentPage; }
private set
{
if (value == _currentPage)
return;
if (_currentPage != null)
_currentPage.IsCurrentPage = false;
_currentPage = value;
if (_currentPage != null)
_currentPage.IsCurrentPage = true;
MovePreviousCommand.RaiseCanExecuteChanged();
MoveNextCommand.RaiseCanExecuteChanged();
OnPropertyChanged("Title");
OnPropertyChanged("CurrentPage");
OnPropertyChanged("IsOnLastPage");
}
}
/// <summary>
/// Returns true if the user is
currently viewing the last page
/// in the workflow. This property is used by CoffeeWizardView
/// to switch the Next button's
text to "Finish" when the user
/// has reached the final page.
/// </summary>
public bool IsOnLastPage
{
get { return CurrentPageIndex == Pages.Count -
1; }
}
/// <summary>
/// Returns a read-only
collection of all page ViewModels.
/// </summary>
private ReadOnlyCollection<WizardBaseViewModel> _pages;
public ReadOnlyCollection<WizardBaseViewModel> Pages
{
get
{
if (_pages == null)
CreatePages();
return _pages;
}
}
#region
Events
/// <summary>
/// Raised when the wizard
should be removed from the UI.
/// </summary>
public event EventHandler RequestClose;
#endregion //
Events
#region
Private Helpers
void CreatePages()
{
_pages = new List<WizardBaseViewModel>
{
new LibraryViewModel(),
new PageTwoViewModel(),
new PageThreeViewModel()
}.AsReadOnly();
}
public string Title
{
get { return CurrentPage == null ? string.Empty : CurrentPage.Title; }
}
int CurrentPageIndex
{
get
{
if (CurrentPage == null)
{
Debug.Fail("Why is the current page
null?");
}
return Pages.IndexOf(CurrentPage);
}
}
void OnRequestClose()
{
EventHandler handler = RequestClose;
if (handler != null)
handler(this, EventArgs.Empty);
}
#endregion
}
За создание
страниц выступает метод CreatePages(). Событие RequestClose используется
для уведомления о закрытии страницы. За активность кнопок Next и
Back
отвечают
команды MovePreviousCommand и MoveNextCommand. После того как мы
реализовали нашу основную модель представления, осталось переопределить метод OnStartup в App.xaml.cs. Результат
запуска мы можем посмотреть на экране.
Первая кнопка неактивна,
так как с первой страницы мы не можем перейти назад.
На третьей странице мы
с помощью стиля, который был приведен из ресурсного файла Resource.xaml, изменили
название кнопки с Next на
Finish.
<Style TargetType="{x:Type Button}" x:Key="moveNextButtonStyle">
<Setter Property="Content" Value="Next" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsOnLastPage}" Value="True">
<Setter Property="Content" Value="Finish" />
</DataTrigger>
</Style.Triggers>
</Style>
Выше приведен повтор стиля,
который изменяет текст кнопки.
На этой позитивной ноте мы завершим создание простого визарда. Очень надеюсь, что после прочтения этой статьи у вас не возникнет проблем с созданием визарда.
На этой позитивной ноте мы завершим создание простого визарда. Очень надеюсь, что после прочтения этой статьи у вас не возникнет проблем с созданием визарда.
Источники:
No comments:
Post a Comment