I'm building an application where a ListBox
is displaying the Description
properties of its items. I want to implement the same kind of edit-in-place functionality that you'd find, say, when editing filenames in Windows Explorer, and I'm finding it to be a whole lot of work.
What I have so far is a ContextMenu
that initiates the edit. It's bound to a command in the view model that sets an IsEditingDescription
property. The item template is styled so that it displays the Description
in a TextBlock
when IsEditingDescription
is false, and in a TextBox
when IsEditingDescription
is true. The setter on Description
sets IsEditingDescription
to false after setting the description.
This works remarkably well. But there are some things that it doesn't do that it should:
- The user should be able to initiate the edit by pressing F2.
- The user should be able to cancel the edit by pressing ESC.
- The user should be able to confirm the edit by pressing ENTER.
- When the user clicks a second time on the currently selected item, it should initiate the edit.
- The text of the description should be selected when the text box appears
I think I can handle the first three items with commands and key bindings, though I don't really understand how to do key bindings yet. But I can't really figure out an MVVMish way to do the other two. Is there one?
this was something I was having to do over and over so I decided to extend the ListView control and added ItemKeyDown, ItemKeyUp and ItemDoubleClick events which are fired where those events occur when an item in the ListView is in focus. The ListView is derived a ListBox so you should be able to port it over pretty easily.
/Fx/ListView.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Controls;
namespace Fx
{
public static class Func
{
/// <summary>
/// Finds a specific type of parent up the visual tree.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="dep"></param>
/// <returns></returns>
public static T FindParent<T>(this object obj)
{
DependencyObject dep = obj as DependencyObject;
while ((dep != null) && !(dep is T))
{
dep = VisualTreeHelper.GetParent(dep);
}
return (dep != null) ? (T)Convert.ChangeType(dep, typeof(T)) : default(T);
}
}
public class ListView:System.Windows.Controls.ListView
{
#region public event KeyboardEventHandler ItemKeyDown;
/// <summary>
/// Occurs when a key is pressed when a ListViewItem in this
/// ListView is in focus.
/// </summary>
public event KeyEventHandler ItemKeyDown;
/// <summary>
/// Raises the ItemKeyDown event for a ListViewitem in this ListView.
/// </summary>
/// <param name="item"></param>
/// <param name="e"></param>
public void OnItemKeyDown(ListViewItem item, KeyEventArgs e)
{
if (ItemKeyDown != null)
ItemKeyDown(item, e);
}
/// <summary>
/// Handle they KeyDown event on the ListView, find the related
/// ListViewItem and raise the ItemKeyDown event respectively.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ListView_KeyDown(object sender, KeyEventArgs e)
{
ListViewItem item = Func.FindParent<ListViewItem>(e.OriginalSource);
if (item != null)
OnItemKeyDown(item, e);
}
#endregion
#region public event KeyboardEventHandler ItemKeyUp;
/// <summary>
/// Occurs when a key is released when a ListViewItem in this
/// ListView is in focus.
/// </summary>
public event KeyEventHandler ItemKeyUp;
/// <summary>
/// Raises the ItemKeyUp event for a ListViewitem in this ListView.
/// </summary>
/// <param name="item"></param>
/// <param name="e"></param>
public void OnItemKeyUp(ListViewItem item, KeyEventArgs e)
{
if (ItemKeyUp != null)
ItemKeyUp(item, e);
}
/// <summary>
/// Handle they KeyUp event on the ListView, find the related
/// ListViewItem and raise the ItemKeyUp event respectively.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ListView_KeyUp(object sender, KeyEventArgs e)
{
ListViewItem item = Func.FindParent<ListViewItem>(e.OriginalSource);
if (item != null)
OnItemKeyUp(item, e);
}
#endregion
#region public event MouseButtonEventHandler ItemDoubleClick;
/// <summary>
/// Occurs when a ListViewItem in this Listview is double clicked.
/// </summary>
public event MouseButtonEventHandler ItemDoubleClick;
/// <summary>
/// Raise the ItemDoubleClick event for a ListViewItem.
/// </summary>
/// <param name="item"></param>
/// <param name="e"></param>
public void OnItemDoubleClick(ListViewItem item, MouseButtonEventArgs e)
{
if (ItemDoubleClick != null)
ItemDoubleClick(item, e);
}
/// <summary>
/// Handle the MouseDoubleClick event for the ListView, find the related
/// ListViewItem and raise the ItemDoubleClick event respectively.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ListView_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
ListViewItem item = Func.FindParent<ListViewItem>(e.OriginalSource);
if (item != null)
OnItemDoubleClick(item, e);
}
#endregion
public ListView()
{
MouseDoubleClick += new MouseButtonEventHandler(ListView_MouseDoubleClick);
KeyDown += new KeyEventHandler(ListView_KeyDown);
KeyUp += new KeyEventHandler(ListView_KeyUp);
}
}
}
Now to use it...
/Pages/EmployeesPage.xaml
<UserControl x:Class="TestApp.Pages.EmployeesPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:fx="clr-namespace:Fx"
Width="800" Height="450">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="List Items" FontWeight="Bold" FontSize="18" />
<!-- here is the main part -->
<fx:ListView x:Name="EmployeesList" ItemDoubleClick="EmployeesList_ItemDoubleClick" ItemKeyDown="EmployeesList_ItemKeyDown" />
<!-- main part ends here -->
</StackPanel>
</Grid>
</UserControl>
/Pages/EmployeesPage.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace TestApp.Pages
{
/// <summary>
/// Interaction logic for EmployeesPage.xaml
/// </summary>
public partial class EmployeesPage : UserControl
{
public EmployeesPage()
{
InitializeComponent();
// Fill the ListView with data...
// EmployeesList.ItemsSource = SomeObservableCollectionOrDataSource
}
private void EmployeesList_ItemDoubleClick(object sender, MouseButtonEventArgs e)
{
MessageBox.Show("an item was double clicked");
ListViewItem item = sender as ListViewItem;
// entityType obj = item.DataContext as entityType
// you can begin editing here
}
private void EmployeesList_ItemKeyDown(object sender, KeyEventArgs e)
{
MessageBox.Show(e.Key.ToString() + " key was pressed on an item");
ListViewItem item = sender as ListViewItem;
// entityType obj = item.DataContext as entityType
if (e.Key == Key.F2)
{
// begin editing here
}
else if (e.Key == Key.Enter)
{
// end editing here
}
else if (e.Key == Key.Escape)
{
// cancel editing here
}
}
}
}
Hope this is of at least some use to you.. Good luck.
As for ensuring the text is selected on entering edit mode, I previously used the following behaviour attached to textboxes in another project of mine. It is in VB, but should be pretty straight-forward.
Public Class SelectAllOnFocusTextboxBehavior
Inherits Behavior(Of TextBox)
Protected Overrides Sub OnAttached()
MyBase.OnAttached()
AddHandler AssociatedObject.GotKeyboardFocus, AddressOf AssociatedObjectGotKeyboardFocus
End Sub
Protected Overrides Sub OnDetaching()
MyBase.OnDetaching()
RemoveHandler AssociatedObject.GotKeyboardFocus, AddressOf AssociatedObjectGotKeyboardFocus
End Sub
Private Sub AssociatedObjectGotKeyboardFocus(ByVal sender As Object, ByVal e As KeyboardFocusChangedEventArgs)
AssociatedObject.SelectAll()
End Sub
End Class