And here we are, back with another installment of our ongoing series of
ListView tutorials. And hey, I think that I got this one out in a
reasonable amount of time - it has only been a month since Part2 was
published (as opposed to the 8 months between Part 2 and Part1). Today we are
going to take a look at how to do in-place editing using a ListView,
which, because the ListView is not a full blown datagrid, takes a fair
amount of work.
Now, I do realize that there is an actual WPF DataGrid control in the
works (and in fact, they just released v1 about a month ago). The plan
is that it will be integrated into the next release of the .NET
framework, but for now you can always grab the source
here
if you want to play around with it. I've poked at it a little bit, and
you never know, maybe I'll write up a tutorial on it at some point. But
for now, we are focusing on the ListView, which is still a very useful
control all on its own.
Of course, we need to start off with a screenshot of what this in place
editing will look like:
Essentially, the selected row will turn into a row of editable controls
(in this case, two textboxes and a combobox). The user can edit them to
their heart's content, their changes being pushed into the backend data
store. As the selected row changes, the controls flip between standard
textblocks and the edit controls.
We will be starting off with the code from the
previous
tutorial, so you might want to review that before you continue.
So first off, we have to make it so that the ListView and the backing
properties are much more tightly bound. This way, changes to backing
properties will be reflected in the ListView, and changes to the
ListView will be reflected in the backing properties. The old backing
class was really simple, and looked like this:
public class GameData
{
public string GameName { get; set; }
public string Creator { get; set; }
public string Publisher { get; set; }
}
Essentially, we need to make all of these properties dependency
properties:
public class GameData : DependencyObject
{
public static readonly DependencyProperty GameNameProperty =
DependencyProperty.Register("GameName", typeof(string),
typeof(GameData), new UIPropertyMetadata(null));
public string GameName
{
get { return (string)GetValue(GameNameProperty); }
set { SetValue(GameNameProperty, value); }
}
public static readonly DependencyProperty CreatorProperty =
DependencyProperty.Register("Creator", typeof(string),
typeof(GameData), new UIPropertyMetadata(null));
public string Creator
{
get { return (string)GetValue(CreatorProperty); }
set { SetValue(CreatorProperty, value); }
}
public static readonly DependencyProperty PublisherProperty =
DependencyProperty.Register("Publisher", typeof(string),
typeof(GameData), new UIPropertyMetadata(null));
public string Publisher
{
get { return (string)GetValue(PublisherProperty); }
set { SetValue(PublisherProperty, value); }
}
}
Yeah, its a lot more code (the main thing I dislike about
DependencyProperties - although that is mitigated with the extremely
handy
propdb
built in Visual Studio code snippet). These are pretty
standard DependencyProperty declarations, but if you need a refresher on
how they work, I suggest you check out our
DependencyProperty
tutorial.
Ok, now that all those properties are DependencyProperties, they will
work for two-way binding. And so now its time to work on the XAML. To
start off with, we are going to take a look at the XAML behind one of
the columns in the ListView:
<GridViewColumn Width="140">
<GridViewColumnHeader Click="SortClick"
Tag="GameName"
Content="Game Name" />
<GridViewColumn.CellTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding Path=GameName}"
Style="{StaticResource GridBlockStyle}"/>
<TextBox Text="{Binding Path=GameName}"
Style="{StaticResource GridEditStyle}" />
</Grid>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
The
GridViewColumnHeader
is the same as it was in the previous
tutorial, but we have gotten rid of the attribute DisplayMemberBinding
on the column itself. In its place, we are setting up a DataTemplate
for the cell. The DataTemplate
consists of two main parts - the
TextBlock and the TextBox. Both are bound to the the same backing field
- the field for this column. They also both have a style set, which is
what we are going to look at next:
First, the style for the
TextBlock
:<Style TargetType="{x:Type TextBlock}"
x:Key="GridBlockStyle">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Visibility"
Value="{Binding Path=IsSelected,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type ListViewItem}},
Converter={StaticResource boolToVis},
ConverterParameter=False}" />
</Style>
The
VerticalAlignment
isn't very important, it just makes the content
look nice. The real important part here is what Visibility
is being
set to. What we want to happen is that the TextBlock is visible when the
row is not selected, but invisible when the row is selected. The
annoying part is finding out if the row a particular TextBlock is in is
selected.
We do this by using a special type of Binding, called RelativeSource.
Essentially, we are telling the binding to walk up the Visual Tree from
the TextBlock to find the first parent that is a
ListViewItem
. Off of
that ListViewItem
, we want the IsSelected
property. We then take
that value and pass it to a converter - and that converter gives us a
Visibility enum back. Let's take a look at that converter:public class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
bool param = bool.Parse(parameter as string);
bool val = (bool)value;
return val == param ?
Visibility.Visible : Visibility.Hidden;
}
public object ConvertBack(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
This converter expects a boolean as the value, and also expects a
boolean (in the form of a string) as the parameter. If the value and the
parameter match, the converter returns
Visibility.Visible
, otherwise
it returns Visibility.Hidden
. This is why the ConverterParameter
for
the visibility binding on the TextBlock is set to false - we want the
TextBlock to be visible when IsSelected
is false.
Ok, now for that TextBox style:
<Style TargetType="{x:Type FrameworkElement}"
x:Key="GridEditStyle">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Visibility"
Value="{Binding Path=IsSelected,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type ListViewItem}},
Converter={StaticResource boolToVis},
ConverterParameter=True}" />
</Style>
This style is not specifically set for a
TextBox
, because we will be
using it for other things (as you will see in a minute). Other than that
it is virtually identical to the style for the TextBox
. The key
difference here is that the ConverterParameter
on the visibility
binding is set to true - in this case, we want the TextBox
to only be
visible when the row is selected.
Ok, let's get crazy and throw the rest of the XAML for the ListView in
here:
<ListView x:Name="gameListView" ItemsSource=
"{Binding ElementName=This, Path=GameCollection}">
<ListView.View>
<GridView>
<GridViewColumn Width="140">
<GridViewColumnHeader Click="SortClick"
Tag="GameName"
Content="Game Name" />
<GridViewColumn.CellTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding Path=GameName}"
Style="{StaticResource GridBlockStyle}"/>
<TextBox Text="{Binding Path=GameName}"
Style="{StaticResource GridEditStyle}" />
</Grid>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Width="140">
<GridViewColumnHeader Click="SortClick"
Tag="Creator"
Content="Creator" />
<GridViewColumn.CellTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding Path=Creator}"
Style="{StaticResource GridBlockStyle}"/>
<TextBox Text="{Binding Path=Creator}"
Style="{StaticResource GridEditStyle}" />
</Grid>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Width="140">
<GridViewColumnHeader Click="SortClick"
Tag="Publisher"
Content="Publisher" />
<GridViewColumn.CellTemplate>
<DataTemplate>
<Grid HorizontalAlignment="Stretch">
<TextBlock Text="{Binding Path=Publisher}"
Style="{StaticResource GridBlockStyle}"/>
<ComboBox SelectedItem="{Binding Path=Publisher}"
ItemsSource="{Binding ElementName=This,
Path=AvailablePublishers}"
Style="{StaticResource GridEditStyle}" />
</Grid>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
Well, we have already walked through the first column step by step, and
the second column is identical (except for the fact that the backing
property being bound to is Creator and not GameName). The third column
is where things get a little bit interesting - the introduction of a
ComboBox
instead of a TextBox
. The code is still very similar too
the first two columns - we get to reuse the same edit control style that
we wrote above. The important differences are that we bind the backing
property to the ComboBox
's SelectedValue
, and that we need to give
the ComboBox
a set of items to actually show as choices (the
ItemsSource
- which we are just setting to a collection of strings
being created in the C# code behind).
Well, that is about it. Below is all the C# and XAML code for this
example:
<Window x:Class="ListViewTest4.ListViewTest" Name="This"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ListViewTest4"
Title="Some Game Data" Height="216" Width="435">
<Window.Resources>
<local:BoolToVisibilityConverter x:Key="boolToVis" />
<Style TargetType="{x:Type TextBlock}"
x:Key="GridBlockStyle">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Visibility"
Value="{Binding Path=IsSelected,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type ListViewItem}},
Converter={StaticResource boolToVis},
ConverterParameter=False}" />
</Style>
<Style TargetType="{x:Type FrameworkElement}"
x:Key="GridEditStyle">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Visibility"
Value="{Binding Path=IsSelected,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type ListViewItem}},
Converter={StaticResource boolToVis},
ConverterParameter=True}" />
</Style>
</Window.Resources>
<StackPanel>
<ListView x:Name="gameListView" ItemsSource=
"{Binding ElementName=This, Path=GameCollection}">
<ListView.View>
<GridView>
<GridViewColumn Width="140">
<GridViewColumnHeader Click="SortClick"
Tag="GameName"
Content="Game Name" />
<GridViewColumn.CellTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding Path=GameName}"
Style="{StaticResource GridBlockStyle}"/>
<TextBox Text="{Binding Path=GameName}"
Style="{StaticResource GridEditStyle}" />
</Grid>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Width="140">
<GridViewColumnHeader Click="SortClick"
Tag="Creator"
Content="Creator" />
<GridViewColumn.CellTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding Path=Creator}"
Style="{StaticResource GridBlockStyle}"/>
<TextBox Text="{Binding Path=Creator}"
Style="{StaticResource GridEditStyle}" />
</Grid>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Width="140">
<GridViewColumnHeader Click="SortClick"
Tag="Publisher"
Content="Publisher" />
<GridViewColumn.CellTemplate>
<DataTemplate>
<Grid HorizontalAlignment="Stretch">
<TextBlock Text="{Binding Path=Publisher}"
Style="{StaticResource GridBlockStyle}"/>
<ComboBox
SelectedItem="{Binding Path=Publisher}"
ItemsSource="{Binding ElementName=This,
Path=AvailablePublishers}"
Style="{StaticResource GridEditStyle}" />
</Grid>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
<Button HorizontalAlignment="Right" Margin="5"
Content="Add Row" Click="AddRowClick" />
</StackPanel>
</Window>
using System;
using System.Windows;
using System.Windows.Controls;
using System.Collections.ObjectModel;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Windows.Media;
using System.Windows.Documents;
using System.Windows.Data;
namespace ListViewTest4
{
public partial class ListViewTest : Window
{
private ObservableCollection<GameData> _GameCollection =
new ObservableCollection<GameData>();
private ObservableCollection<string> _AvailablePublishers =
new ObservableCollection<string>();
private GridViewColumnHeader _CurSortCol = null;
private SortAdorner _CurAdorner = null;
public ListViewTest()
{
_GameCollection.Add(new GameData {
GameName = "World Of Warcraft",
Creator = "Blizzard",
Publisher = "Blizzard" });
_GameCollection.Add(new GameData {
GameName = "Halo",
Creator = "Bungie",
Publisher = "Microsoft" });
_GameCollection.Add(new GameData {
GameName = "Gears Of War",
Creator = "Epic",
Publisher = "Microsoft" });
_AvailablePublishers.Add("Microsoft");
_AvailablePublishers.Add("Blizzard");
_AvailablePublishers.Add("Nintendo");
_AvailablePublishers.Add("Electronic Arts");
_AvailablePublishers.Add("Activision");
_AvailablePublishers.Add("Ubisoft");
_AvailablePublishers.Add("Take-Two Interactive");
InitializeComponent();
}
public ObservableCollection<GameData> GameCollection
{ get { return _GameCollection; } }
public ObservableCollection<string> AvailablePublishers
{ get { return _AvailablePublishers; } }
private void AddRowClick(object sender, RoutedEventArgs e)
{
_GameCollection.Add(new GameData {
GameName = "A New Game",
Creator = "A New Creator",
Publisher = "<Select A Publisher>" });
}
private void SortClick(object sender, RoutedEventArgs e)
{
GridViewColumnHeader column =
sender as GridViewColumnHeader;
String field = column.Tag as String;
if (_CurSortCol != null)
{
AdornerLayer.GetAdornerLayer(_CurSortCol).Remove(_CurAdorner);
gameListView.Items.SortDescriptions.Clear();
}
ListSortDirection newDir = ListSortDirection.Ascending;
if (_CurSortCol == column && _CurAdorner.Direction == newDir)
newDir = ListSortDirection.Descending;
_CurSortCol = column;
_CurAdorner = new SortAdorner(_CurSortCol, newDir);
AdornerLayer.GetAdornerLayer(_CurSortCol).Add(_CurAdorner);
gameListView.Items.SortDescriptions.Add(
new SortDescription(field, newDir));
}
}
public class GameData : DependencyObject
{
public static readonly DependencyProperty GameNameProperty =
DependencyProperty.Register("GameName", typeof(string),
typeof(GameData), new UIPropertyMetadata(null));
public string GameName
{
get { return (string)GetValue(GameNameProperty); }
set { SetValue(GameNameProperty, value); }
}
public static readonly DependencyProperty CreatorProperty =
DependencyProperty.Register("Creator", typeof(string),
typeof(GameData), new UIPropertyMetadata(null));
public string Creator
{
get { return (string)GetValue(CreatorProperty); }
set { SetValue(CreatorProperty, value); }
}
public static readonly DependencyProperty PublisherProperty =
DependencyProperty.Register("Publisher", typeof(string),
typeof(GameData), new UIPropertyMetadata(null));
public string Publisher
{
get { return (string)GetValue(PublisherProperty); }
set { SetValue(PublisherProperty, value); }
}
}
public class SortAdorner : Adorner
{
private readonly static Geometry _AscGeometry =
Geometry.Parse("M 0,5 L 10,5 L 5,0 Z");
private readonly static Geometry _DescGeometry =
Geometry.Parse("M 0,0 L 10,0 L 5,5 Z");
public ListSortDirection Direction { get; private set; }
public SortAdorner(UIElement element, ListSortDirection dir)
: base(element)
{ Direction = dir; }
protected override void OnRender(
DrawingContext drawingContext)
{
base.OnRender(drawingContext);
if (AdornedElement.RenderSize.Width < 20)
return;
drawingContext.PushTransform(
new TranslateTransform(
AdornedElement.RenderSize.Width - 15,
(AdornedElement.RenderSize.Height - 5) / 2));
drawingContext.DrawGeometry(Brushes.Black, null,
Direction == ListSortDirection.Ascending ?
_AscGeometry : _DescGeometry);
drawingContext.Pop();
}
}
public class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
bool param = bool.Parse(parameter as string);
bool val = (bool)value;
return val == param ? Visibility.Visible : Visibility.Hidden;
}
public object ConvertBack(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
You can grab the Visual Studio project for the above example
here. The
project includes all the examples for all the ListView tutorials, so you
should be able to easily see how the code got to where it is now.
Source Files:
Comments
Post a Comment