Movies.WPF Project
In this section, we will work on Movies.WPF project, a WPF application to display and manage a database of movies.
Project Setup
Make the following changes to Movies.WPF project:
Step 1. Add project reference Movies to this project.
Step 2. Add NuGet package DevZest.Data.WPF to this project.
Step 3. Add the following assembly level attribute to Properties/AssemblyInfo.cs (or My Project/AssemblyInfo.vb if you're using VB.Net):
using DevZest.Data.Presenters;
using Movies.WPF;
...
[assembly: ResourceIdRelativeTo(typeof(App))]
Note
If you cannot find AssemblyInfo.vb in Solution Explorer, make sure you have enabled "Show all files" on the toolbar of the Solution Explorer.
Step 4. Add Movies.mdf and Movies_log.ldf to this project as linking, by right clicking Movies.WPF project in Solution Explorer tool window, then click context menu item "Add" -> "Existing Item...", select All Files(.) from right bottom combobox, then navigate to LocalDb subfolder of Movies.DbDesign project, select file Movies.mdf and Movies_log.ldf:
Click the little dropdown arrow next to the Add button and select Add as Link, instead of copying the file into the project directory, Visual Studio will create a link to the original:
Select these two files, then right click and select context menu item Properties, make sure Copy to Output Directory is set to Copy Always:
App/Application
Change App.xaml.cs(Application.xaml.vb if you're using VB.Net) to:
using System;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows;
namespace Movies.WPF
{
public partial class App : Application
{
private static readonly string ConnectionString = GetConnectionString();
private static string GetConnectionString()
{
string mdfFilename = "Movies.mdf";
string outputFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
string attachDbFilename = Path.Combine(outputFolder, mdfFilename);
return string.Format(@"Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=""{0}"";Integrated Security=True", attachDbFilename);
}
private static Db CreateDb()
{
return new Db(App.ConnectionString);
}
public static async Task ExecuteAsync(Func<Db, Task> func)
{
using (var db = CreateDb())
{
await func(db);
}
}
public static async Task<T> ExecuteAsync<T>(Func<Db, Task<T>> func)
{
using (var db = CreateDb())
{
return await func(db);
}
}
}
}
MovieDetailWindow
Add class file MovieDetailWindow.Presenter.cs (MovieDetailWindow.Presenter.vb if you're using VB.Net):
using DevZest.Data.Presenters;
using System.Threading.Tasks;
namespace Movies.WPF
{
partial class MovieDetailWindow
{
private sealed class Presenter : DataPresenter<Movie>
{
private const string LABEL_FORMAT = "{0}:";
protected override void BuildTemplate(TemplateBuilder builder)
{
var title = _.Title.BindToTextBox();
var releaseDate = _.ReleaseDate.BindToDatePicker();
var genre = _.Genre.BindToTextBox();
var price = _.Price.BindToTextBox();
builder
.GridColumns("Auto", "*")
.GridRows("Auto", "Auto", "Auto", "Auto")
.AddBinding(0, 0, _.Title.BindToLabel(title, LABEL_FORMAT))
.AddBinding(0, 1, _.ReleaseDate.BindToLabel(releaseDate, LABEL_FORMAT))
.AddBinding(0, 2, _.Genre.BindToLabel(genre, LABEL_FORMAT))
.AddBinding(0, 3, _.Price.BindToLabel(price, LABEL_FORMAT))
.AddBinding(1, 0, title)
.AddBinding(1, 1, releaseDate)
.AddBinding(1, 2, genre)
.AddBinding(1, 3, price);
}
public int ID
{
get { return _.ID[0].Value; }
}
public bool IsNew
{
get { return ID < 1; }
}
public Task SaveToDbAsync()
{
if (IsNew)
return App.ExecuteAsync(db => db.Movie.InsertAsync(DataSet));
else
return App.ExecuteAsync(db => db.Movie.UpdateAsync(DataSet));
}
}
}
}
Add window MovieDetailWindow.xaml to project. Change MovieDetailWindow.xaml to:
<Window x:Class="Movies.WPF.MovieDetailWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Movies.WPF"
xmlns:dz="http://schemas.devzest.com/data/windows"
mc:Ignorable="d"
Title="Movie" Height="230" Width="300" WindowStartupLocation="CenterOwner">
<Window.Resources>
<Style TargetType="Label">
<Setter Property="Margin" Value="5,5,0,5" />
<Setter Property="HorizontalAlignment" Value="Right" />
</Style>
<Style TargetType="TextBox">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Margin" Value="5,0,5,0" />
</Style>
<Style TargetType="DatePicker">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Margin" Value="5,0,5,0" />
</Style>
<Style TargetType="Button">
<Setter Property="Margin" Value="5" />
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<dz:DataView x:Name="_dataView" Background="White" Margin="5" />
<UniformGrid Width="180" Columns="2" HorizontalAlignment="Right" VerticalAlignment="Bottom" Grid.Row="1">
<Button Content="OK" IsDefault="True" Command="{x:Static local:MovieDetailWindow+Commands.Submit}" />
<Button Content="Cancel" IsCancel="True" />
</UniformGrid>
</Grid>
</Window>
Change code behind MovieDetailWindow.xaml.cs (MovieDetailWindow.xaml.vb if you're using VB.Net) to:
using DevZest.Data;
using System.Diagnostics;
using System.Windows;
using System.Windows.Input;
namespace Movies.WPF
{
public partial class MovieDetailWindow : Window
{
public static class Commands
{
public static readonly RoutedCommand Submit = new RoutedCommand(nameof(Submit), typeof(MovieDetailWindow));
}
public static bool Show(DataSet<Movie> data, Window ownerWindow)
{
return new MovieDetailWindow().ShowDialog(data, ownerWindow);
}
public MovieDetailWindow()
{
InitializeComponent();
CommandBindings.Add(new CommandBinding(Commands.Submit, ExecSubmit, CanExecSubmit));
}
private Presenter _presenter;
private async void ExecSubmit(object sender, ExecutedRoutedEventArgs e)
{
if (_presenter.IsEditing)
_presenter.CurrentRow.EndEdit();
if (!_presenter.SubmitInput())
return;
await _presenter.SaveToDbAsync();
DialogResult = true;
Close();
}
private void CanExecSubmit(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = _presenter.CanSubmitInput;
}
private bool ShowDialog(DataSet<Movie> data, Window ownerWindow)
{
Debug.Assert(data.Count == 1);
_presenter = new Presenter();
_presenter.Show(_dataView, data);
Owner = ownerWindow;
Title = _presenter.IsNew ? "New Movie" : string.Format("Movie: {0}", _presenter.ID);
return ShowDialog().Value;
}
}
}
MainWindow
Add class file MainWindow.Presenter.cs (MainWindow.Presenter.vb if you're using VB.Net):
using DevZest.Data;
using DevZest.Data.Presenters;
using DevZest.Data.Views;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Controls;
namespace Movies.WPF
{
partial class MainWindow
{
public static class Styles
{
public static readonly StyleId CheckBox = new StyleId(typeof(MainWindow));
public static readonly StyleId LeftAlignedTextBlock = new StyleId(typeof(MainWindow));
public static readonly StyleId RightAlignedTextBlock = new StyleId(typeof(MainWindow));
}
public sealed class Presenter : DataPresenter<Movie>
{
public interface IFilter
{
string Text { get; set; }
}
public Presenter(IFilter filter)
{
_filter = filter;
}
private readonly IFilter _filter;
private Task<DataSet<Movie>> LoadDataAsync(CancellationToken ct)
{
return App.ExecuteAsync(db => db.GetMoviesAsync(_filter.Text, ct));
}
public void ShowAsync(DataView dataView)
{
ShowAsync(dataView, LoadDataAsync);
}
public Task RefreshAsync(bool clearFiler)
{
if (clearFiler)
_filter.Text = null;
return RefreshAsync(LoadDataAsync);
}
protected override void BuildTemplate(TemplateBuilder builder)
{
builder.GridColumns("20", "*", "Auto", "Auto", "Auto")
.GridRows("Auto", "Auto")
.Layout(Orientation.Vertical)
.WithFrozenTop(1)
.AddBinding(0, 0, this.BindToCheckBox().WithStyle(Styles.CheckBox))
.AddBinding(1, 0, _.Title.BindToColumnHeader())
.AddBinding(2, 0, _.ReleaseDate.BindToColumnHeader())
.AddBinding(3, 0, _.Genre.BindToColumnHeader())
.AddBinding(4, 0, _.Price.BindToColumnHeader())
.AddBinding(0, 1, _.BindToCheckBox().WithStyle(Styles.CheckBox))
.AddBinding(1, 1, _.Title.BindToHyperlink(Commands.Open).WithStyle(Styles.LeftAlignedTextBlock))
.AddBinding(2, 1, _.ReleaseDate.BindToTextBlock("{0:d}").WithStyle(Styles.RightAlignedTextBlock))
.AddBinding(3, 1, _.Genre.BindToTextBlock().WithStyle(Styles.LeftAlignedTextBlock))
.AddBinding(4, 1, _.Price.BindToTextBlock("{0:C}").WithStyle(Styles.RightAlignedTextBlock));
}
}
}
}
Add resource dictionary MainWindow.Styles.xaml:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Movies.WPF">
<Style x:Key="DefaultTextBlock" TargetType="TextBlock">
<Setter Property="Padding" Value="2" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style x:Key="{x:Static local:MainWindow+Styles.CheckBox}" TargetType="CheckBox">
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style x:Key="{x:Static local:MainWindow+Styles.LeftAlignedTextBlock}" TargetType="TextBlock" BasedOn="{StaticResource DefaultTextBlock}">
<Setter Property="TextAlignment" Value="Left" />
</Style>
<Style x:Key="{x:Static local:MainWindow+Styles.RightAlignedTextBlock}" TargetType="TextBlock" BasedOn="{StaticResource DefaultTextBlock}">
<Setter Property="TextAlignment" Value="Right" />
</Style>
</ResourceDictionary>
Change MainWindow.xaml to:
<Window x:Class="Movies.WPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:dz="http://schemas.devzest.com/data/windows"
xmlns:local="clr-namespace:Movies.WPF"
mc:Ignorable="d"
Title="Movies.WPF" Height="300" Width="500" WindowStartupLocation="CenterScreen">
<DockPanel>
<ToolBar DockPanel.Dock="Top">
<Button Command="{x:Static local:MainWindow+Commands.New}">New</Button>
<Separator />
<Button Command="{x:Static local:MainWindow+Commands.Delete}">Delete</Button>
<Separator />
<Button Command="{x:Static local:MainWindow+Commands.Refresh}">Refresh</Button>
<Separator />
<TextBox x:Name="_textBoxSearch" Width="150" />
<Button Command="{x:Static local:MainWindow+Commands.Refresh}">Search</Button>
<Separator />
</ToolBar>
<dz:DataView x:Name="_dataView" />
</DockPanel>
</Window>
Change code behind MainWindow.xaml.cs (MainWindow.xaml.vb if you're using VB.Net) to:
using DevZest.Data;
using System.Diagnostics;
using System.Windows;
using System.Windows.Input;
namespace Movies.WPF
{
public partial class MainWindow : Window, MainWindow.Presenter.IFilter
{
public static class Commands
{
public static RoutedUICommand New { get { return ApplicationCommands.New; } }
public static RoutedUICommand Open { get { return ApplicationCommands.Open; } }
public static RoutedUICommand Delete { get { return ApplicationCommands.Delete; } }
public static RoutedUICommand Refresh { get { return NavigationCommands.Refresh; } }
}
private readonly Presenter _presenter;
public MainWindow()
{
InitializeComponent();
InitializeCommandBindings();
_presenter = new Presenter(this);
_presenter.ShowAsync(_dataView);
}
string Presenter.IFilter.Text
{
get { return _textBoxSearch.Text; }
set { _textBoxSearch.Text = value; }
}
private void InitializeCommandBindings()
{
CommandBindings.Add(new CommandBinding(Commands.New, New));
CommandBindings.Add(new CommandBinding(Commands.Open, Open, CanOpen));
CommandBindings.Add(new CommandBinding(Commands.Delete, Delete, CanDelete));
CommandBindings.Add(new CommandBinding(Commands.Refresh, Refresh, CanRefresh));
}
private void ShowMovieDetailWindow(DataSet<Movie> movie)
{
Debug.Assert(movie.Count == 1);
var result = MovieDetailWindow.Show(movie, this);
if (result)
Refresh(true);
}
private void New(object sender, ExecutedRoutedEventArgs e)
{
var movie = DataSet<Movie>.Create();
movie.AddRow();
ShowMovieDetailWindow(movie);
}
private Movie _
{
get { return _presenter._; }
}
private async void Open(object sender, ExecutedRoutedEventArgs e)
{
var ID = _presenter.CurrentRow.GetValue(_.ID).Value;
var movie = await App.ExecuteAsync(db => db.GetMovieAsync(ID));
ShowMovieDetailWindow(movie);
}
private void CanOpen(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = _presenter.CurrentRow != null;
}
private async void Delete(object sender, ExecutedRoutedEventArgs e)
{
var selectedRows = _presenter.SelectedRows;
var json = _presenter.DataSet.Filter(JsonFilter.PrimaryKeyOnly).ToJsonString(_presenter.SelectedDataRows, false);
var keys = DataSet<Movie.Key>.ParseJson(json);
await App.ExecuteAsync(db => db.Movie.DeleteAsync(keys, (s, _) => s.Match(_)));
Refresh(false);
}
private void CanDelete(object sender, CanExecuteRoutedEventArgs e)
{
var selectedRows = _presenter.SelectedRows;
e.CanExecute = selectedRows != null && selectedRows.Count > 0;
}
private void Refresh(object sender, ExecutedRoutedEventArgs e)
{
Refresh(false);
}
private void Refresh(bool clearFilter)
{
_presenter.RefreshAsync(clearFilter);
}
private void CanRefresh(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = _presenter.DataSet != null;
}
}
}
Run
Press F5 to run project Movies.WPF. You should have a working WPF application to display and manage a database of movies: