Skip to main content

C# WPF Tutorial - Using The ListView, Part 3 [Intermediate]


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:

List View In Place Edit Screenshot

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

Popular posts from this blog

C# Snippet - Shuffling a Dictionary [Beginner]

Randomizing something can be a daunting task, especially with all the algorithms out there. However, sometimes you just need to shuffle things up, in a simple, yet effective manner. Today we are going to take a quick look at an easy and simple way to randomize a dictionary, which is most likely something that you may be using in a complex application. The tricky thing about ordering dictionaries is that...well they are not ordered to begin with. Typically they are a chaotic collection of key/value pairs. There is no first element or last element, just elements. This is why it is a little tricky to randomize them. Before we get started, we need to build a quick dictionary. For this tutorial, we will be doing an extremely simple string/int dictionary, but rest assured the steps we take can be used for any kind of dictionary you can come up with, no matter what object types you use. Dictionary < String , int > origin = new Dictionary < string , int >(); ...

C# WPF Printing Part 2 - Pagination [Intermediate]

About two weeks ago, we had a tutorial here at SOTC on the basics of printing in WPF . It covered the standard stuff, like popping the print dialog, and what you needed to do to print visuals (both created in XAML and on the fly). But really, that's barely scratching the surface - any decent printing system in pretty much any application needs to be able to do a lot more than that. So today, we are going to take one more baby step forward into the world of printing - we are going to take a look at pagination. The main class that we will need to do pagination is the DocumentPaginator . I mentioned this class very briefly in the previous tutorial, but only in the context of the printing methods on PrintDialog , PrintVisual (which we focused on last time) and PrintDocument (which we will be focusing on today). This PrintDocument function takes a DocumentPaginator to print - and this is why we need to create one. Unfortunately, making a DocumentPaginator is not as easy as...

C# WPF Tutorial - Implementing IScrollInfo [Advanced]

The ScrollViewer in WPF is pretty handy (and quite flexible) - especially when compared to what you had to work with in WinForms ( ScrollableControl ). 98% of the time, I can make the ScrollViewer do what I need it to for the given situation. Those other 2 percent, though, can get kind of hairy. Fortunately, WPF provides the IScrollInfo interface - which is what we will be talking about today. So what is IScrollInfo ? Well, it is a way to take over the logic behind scrolling, while still maintaining the look and feel of the standard ScrollViewer . Now, first off, why in the world would we want to do that? To answer that question, I'm going to take a an example from a tutorial that is over a year old now - Creating a Custom Panel Control . In that tutorial, we created our own custom WPF panel (that animated!). One of the issues with that panel though (and the WPF WrapPanel in general) is that you have to disable the horizontal scrollbar if you put the panel in a ScrollV...