InplaceEditBoxLib moved to GitHub at:Dirkster99/InplaceEditBoxLib
Project moved to CodePlex: http://fsc.codeplex.com/documentation and will be moved for more development to GitHub soon; https://github.com/Dirkster99
Figure 1 an artistic view on an overlay textbox adorner ("New Light") displayed over an item in a treeview control. The other box ("Invalid Character Input") sketches a pop-up control that is displayed when the user hits an unsupported key.
Introduction
The development of an Explorer tool window that integrates the Windows file system into an editor required me to integrate a textbox overlay control to rename files and folders in the same fashion as Windows Explorer does it. I was not able to find an acceptable and complete WPF/MVVM implementation - just sketches here and there [1][2] - so I developed my own version and document it here in the hope that it might be useful for others.
This article is structured in 2 blocks - Using the Code and Features. The first block describes the control from a technical point of view while the second part a lists requirements for this implementation.
The demo project attached to this article does actually rename files and folders. The implementation is very stable but you should use extra care about testing this with folders and files in your file system. The actual control described here is the InplaceEditBoxLib/Views/EditBox.* control, which can be found in its folder within the downloadable source code.
You can see that the solution contains 5 other projects besides the InplaceEditBoxLib project. These projects are based on a different project and article which can be found here: A WPF File ListView and ComboBox (Version II). The scope of this article is on the EditBox
control only, but you are of course more than welcome to ask questions about these parts also.
The demo project attached to this article contains 2 demo applications in the FileListViewTest and TestFolderBrowser project. It is probably best to set either project as start-up project and execute it in Visual Studio before looking anywhere else.
Be sure to double click on folders in the folder browser (view or dialog) or use the context menu to execute the Rename and New folder commands that demo the EditBox control for this article.
The artistic image in Figure 1 gives an idea of the concept that is discussed in this article. The concept is that each item in an ItemsControl
(which is the base of TreeView
, ListBox
, ... etc.) can be displayed with a custom control defined in InplaceEditBoxLib.Views.EditBox instead of using a Label
or TextBlock
control.
This concept can be verified with the following XAML code for a TreeView
definition (based on "FolderBrowser/FolderBrowser/Views/FolderBrowserView.xaml"):
<HierarchicalDataTemplate ItemsSource="{Binding Folders}">
<StackPanel Orientation="Horizontal">
<Image ... </Image>
<EditInPlace:EditBox
Text="{Binding Path=FolderName, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
DisplayText="{Binding DisplayItemString, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
ToolTip="{Binding FolderPath, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
Focusable="True"
VerticalAlignment="Stretch"
HorizontalAlignment="Left"
IsReadOnly="{Binding IsReadOnly}"
RenameCommand="{Binding Path=Data.RenameCommand, Source={StaticResource DataContextProxy}}"
ToolTipService.ShowOnDisabled="True"
InvalidInputCharacters="{x:Static loc:Strings.ForbiddenRenameKeys}"
InvalidInputCharactersErrorMessage="{x:Static loc:Strings.ForbiddenRenameKeysMessage}"
InvalidInputCharactersErrorMessageTitle="{x:Static loc:Strings.ForbiddenRenameKeysTitle}"
Margin="2,0" />
</StackPanel>
</HierarchicalDataTemplate>
...or a Listbox
XAML definition (based on "FileListViewTest/FileListItemView.xaml"):
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>...</Grid.ColumnDefinitions>
<Image > ... </Image>
<EditInPlace:EditBox Grid.Column="1"
Text="{Binding DisplayName}"
DisplayText="{Binding DisplayName}"
RenameCommand="{Binding Path=Data.RenameCommand, Source={StaticResource DataContextProxy}}"
ToolTipService.ShowOnDisabled="True"
InvalidInputCharacters="{x:Static fvloc:Strings.ForbiddenRenameKeys}"
InvalidInputCharactersErrorMessage="{x:Static fvloc:Strings.ForbiddenRenameKeysMessage}"
InvalidInputCharactersErrorMessageTitle="{x:Static fvloc:Strings.ForbiddenRenameKeysTitle}"
IsEditableOnDoubleClick ="False"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
Take a note of the RenameCommand
in either XAML definition above. This command is called when a user has successfully edit an item's text and hits enter to confirm his edit.
The above XAML code is not extremely exotic if you have done WPF for a while. It is of course a powerful feature to template every item of an ItemsControl
, especially since this can also take actual property values or types of the objects into account. But the EditBox
control described in this article makes only use of this WPF feature, while the focus of this article is on another WPF feature called Adorner.
The EditBox
control displays a TextBlock
when it is in non-editing mode and it only displays an Adorner with a TextBox
when it is in editing mode. The editing mode is also supported by a measuring TextBlock
control that is not displayed in the above figure since it is never visible. The XAML definition of the EditBox control looks like this (based on InplaceEditBoxLib/Views/EditBox.xaml):
<Style TargetType="{x:Type local:EditBox}" >
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:EditBox}">
<Grid>
<TextBlock x:Name="PART_TextBlock" MinWidth="10"
HorizontalAlignment="Left"
VerticalAlignment="Center"
/>
<TextBlock x:Name="PART_MeasureTextBlock" MinWidth="10"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Visibility="Hidden"
/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The measuring TextBlock
is used to measure the size of a string as the user types a string into the textbox. This part of the control is never visible, but it is part of the control definition to give application developers a chance for using the correct measuring parameters (eg: Font Size, Font Family etc) or just stick with the defaults.
The view part of the EditBox
control is mainly implemented in:
InplaceEditBoxLib.Views.EditBox
InplaceEditBoxLib.Views.EditBoxAdorner
The visibility of the EditBoxAdorner
is controlled by the OnSwitchToEditingMode
and OnSwitchToNormalMode
methods in EditBox.cs.xaml
. The first method establishs the edit mode while the second method is called when the editing mode ends. The OnSwitchToNormalMode
method also calls a bound command (if any) to let the underlying viewmodel/model take care of the actual renaming process in the file system:
if (bCancelEdit == false)
{
if (this.mTextBox != null)
{
if (this.RenameCommand != null)
{
var tuple = new Tuple<string, object>(sNewName, this.DataContext);
this.RenameCommand.Execute(tuple);
}
}
}
The EditBoxAdorner
is pretty much the adorner that was originally suggest by the ATC team [1]. I only changed some details in the BuildTextBox
, MeasureOverride
, and ArrangeOverride
methods to implement the binding and measuring with the measuring textblock when the adorner is visible.
ViewModel Details
The EditBox
control supports a number of dependency properties listed further below in the features section. But it is also important to note that it expects a viewmodel that implements the interfaces:
IEditBox
INotifyableViewModel
(implemented by IEditBox
)
...at its DataContext
(see InplaceEditBoxLib/Interfaces/IEditBox.cs and OnDataContextChanged
method in InplaceEditBoxLib/Views/EditBox.xaml.cs).
The IEditBox
interface implements 2 events, the ShowNotificationMessage
event to show a pop-up notification [4] when error messages should be shown, and the RequestEdit
event to start editing when the viewmodel requests this. Both events can be easily implemented by inheriting an item's viewmodel from InplaceEditBoxLib/ViewModels/EditInPlaceViewModel.cs. Raising an event is then nothing more than calling any of the provided RequestEditMode
or ShowNotification
methods (see for example catch block of RenameFolder
method in FolderBrowser/FolderBrowser/ViewModels/FolderViewModel.cs).
try
{
...
}
catch (Exception exp)
{
base.ShowNotification(FileSystemModels.Local.Strings.STR_RenameFolderErrorTitle, exp.Message);
}
Showing a nice notification could hardly be easier when MVVM is to be designed into the code. It is clear that the layer model (model, viewmodel, view) ensures we re-use this control for many other scenario in which items within an ItemsControl
should be editable with a WPF In-Place-Edit textbox control.
So, wrapping up on the technical section. Have a look into relevant the viewmodels to understand how the EditBox
is driven from there. This can be done by using Visual Studio to find all viewmodels that inherit from the InplaceEditBoxLib.ViewModels.EditInPlaceViewModel
class. Next you can search for all calls to the methods in this viewmodel to uncover the processing logic in each concrete viewmodel implementation.
The processing logic in InplaceEditBoxLib.ViewModels.EditInPlaceViewModel
is tightly coupled (through InplaceEditBoxLib.Interfaces.IEditBox
) with the view in EditBox.cs.xaml
and EditBoxAdorner.cs
.
The edit-in-place text control described in this article can be used as a base for developing applications where users would like to edit text strings as overlay over the normally displayed string.
The best and well known example of an edit-in-place text control is the textbox overlay that is used for renaming files or folders in Windows Explorer. The user typically selects an item in a list (listbox, listview, grid) or structure of items (treeview) and renames the item using a textbox overlay (without an additional dialog).
The edit-in-place control in this project can be used in a WPF collection of any ItemsControl (Treeview, ListBox, ListView etc). A change of focus (activation of a different window) or pressing the Escape key results in canceling of the rename process and pressing enter leads to a confirmation of the new string. This section describes the features of the control such that application developers/users should be able to re-use/use the control described in this article.
The Known Limitations section at the end of this section describes the features that I could not get implemented, yet. Please have a look at it and contribute a solution if you can remove any of the listed limitations.
Start Editing Text with Context Menu
The EditBox
control can start editing text through its own user interface (via double click) or via 'external' accessibility (context menu or menu).
The edit-in-place control expects the viewmodel to implement the InplaceEditBoxLib.Interfaces.IEditBox
interface which contains a RequestEdit
event. This event can be fired by the viewmodel to start editing of an item. The IsEditing
dependency property can than be used to determine whether the editing mode is currently active or not.
Editing can be started through different routes, but the application should display an overlay textbox control in either way, and that textbox should contain all the current text being selected. This state is referred to as edit mode.
Editing text with Text and DisplayText properties
The EditBox
control has 2 string properties, one is for display (DisplayText) and the other (Text) string represents the value that should be edited.
This setup enables application developers to show more than just a name in each item. Each item can, for example, display a name and a number (eg: "Myfiles (123 entries)") by using the DisplayText property, while the Text property should contain the string that is to be edit (eg: "Myfiles").
The confirmation of editing (pressing enter) does not change either of the above dependency properties. The edit-in-place control executes instead the command that is bound to the RenameCommand dependency property to let the viewmodel adjust all relevant strings.
The view generates a command parameter for this command (cannot be configured). The parameter is a Tuple
of the new string and the viewmodel instance that is available at the DataContext
of the edit-in-place control.
Usage of Limited Space
The EditBox in-place overlay control should not exceed the view port area of the parent scrollviewer of the ItemsControl
. That is, the EditBox
should not exceed the visible area of a treeview if it was used within a treeview. This rule ensures that users do not end up typing in an invisible area (off-screen) when typing long strings.
The following sequence of images shows the application behavior when the user enters the string "The quick fox jumps over the river" in a limited space scenario:
Cancel and Confirm
Editing text with the edit-in-place control can be canceled by pressing the Esc key or changing the input focus to another window or control. The application shows the text as it was before the editing started.
Editing text can be confirmed pressing the enter key. The application shows the entered text instead of the text before the editing started.
IsReadOnly property
The edit-in-place control supports a Boolean IsReadonly dependency property to lock individual items from being renamed. Default is false meaning every item is editable unless binding defines somthing else.
IsEditableOnDoubleClick
Editing the string that is displayed with the edit-in-place control can be triggered with a timed 'double click'. This double click can be configured to occur in a certain time frame. There are 2 double dependency properties that can be setup to consume only those double clicks with a time frame that is larger than MinimumClickTime but smaller than MaximumClickTime.
Default values for MinimumClickTime and MaximumClickTime are 300 ms and 700 ms, respectively.
The IsEditableOnDoubleClick boolean dependency property can be setup to dermine whether double clicks are evaluated for editing or not. Default is true.
IsEditing property
The edit-in-place control supports a one way Boolean IsEditing
dependency property to enable viewmodels to determine whether an item is currently edited or not. This property cannot be used by the viewmodel to force the view into editable mode (since it is a get only property in the view).
Use the RequestEdit event defined in InplaceEditBoxLib.Interfaces.IEditBox to request an edit mode that is initialized by the viewmodel and use the IsEditing
property to verify the current edit mode in the viewmodel.
Key Filter and Error Handling
The EditBox control contains properties that can be used to define a blacklist of characters that should not be input by the user. See properties:
- InvalidInputCharacters
Use this property to specifiy the actual set of characters which users should not be able to enter.
- InvalidInputCharactersMessage
Use this property to set a message string that is display when a user hits a key that is not supported.
- InvalidInputCharactersTitle
Use this property to set a title string that is display when a user hits a key that is not supported.
The EditBox
control implements a pop-up message element to show hints to the user when he types invalid characters.
So, each item in a viewmodel can have its own pop-up notification and black list through the IEditBox
interface implemented in an items viewmodel. But what about showing a notification when there is no item to display, yet (eg: when creating a new folder in an empty folder that has no create folder permission)? This case is covered with the implementation of a NotifableContentControl
[4]. This control is based on a ContentControl
and can thus be wrapped around any other control. The control implements the ShowNotification
event which can be used to show notifications in the context of any other control.
See FileListViewTest/BrowserAndFileListView.xaml for more details:
<view:NotifyableContentControl
DataContext="{Binding SynchronizedFolderView.FolderItemsView}"
Notification="{Binding Notification}" Margin="0,26,0,2" Grid.RowSpan="2">
<ListBox ... >/ListBox>
</view:NotifyableContentControl>
Known Limitations
- Pressing the F2 key on a selected
Treeview
or Listbox
item does not start the editing mode.
- Clicking on the background of the
ItemsControl
(TreeView, ListView etc) does not cancel the edit mode.
- Restyling TextBox with Hyperlink does not work since a Hyperlink is stored in the
InlineCollection
of a TextBox. But an InlineCollection
cannot be set via dependency property and I cannot seem to work around this with a custom dependency property.
- Key definitions entered in the in-place textbox cannot be defined through a white-list. The textbox does not support input masks.
Points of Interest
I have learned using adorners with more confidience. I also leaned that adorners can only appear over the real estate of their hosting control's window (An adorner is drawn on the adorner layer of a control (AdornerLayer.GetAdornerLayer()), which is located depended on where you defined the AdornerLayer. But the topmost control you can define is a Window.). It is therefore, more appropriate to have a pop-up message control instead of another adorner for displaying the error notification.
Implementing the pop-up notification was driven by a previously developed project that came in handy when I needed it here. Using an interface definition between view and viewmodel to forward the event from viewmodel to view was one of the many tricks and patterns I take away from here.
I had a nagging feeling that an event could result in a more stable implementation than using an IsEditing
dependency property for switching in and out of editing mode. The stability of this implementation proofs me right.
The ContentControl
pattern for extending functionality of available controls with Adorners was new to me. The advantage of this approach is that it can easily be used when states and bindings are required. Atteched behaviors may also be considered if the requested extension is simple enough to implement.
References
History
30-07-2014 Created first version of this article