Click here to Skip to main content
15,886,199 members
Articles / Web Development / HTML

A WPF MVVM In-Place-Edit TextBox Control

Rate me:
Please Sign up or sign in to vote.
4.96/5 (18 votes)
2 Sep 2017CPOL13 min read 51.9K   1.8K   37   1
Rename an item via textbox overlay as in Rename of Windows Explorer

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

 

Image 1
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.

Solution Overview

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.

Image 2

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.

Using the Code

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.

Image 3Image 4

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)
  {
    // Tell the ViewModel (if any) that we'd like to rename this item
    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.

Summary

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.

Features

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:

Image 5 Image 6 Image 7 Image 8 Image 9

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.

Image 10

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Germany Germany
The Windows Presentation Foundation (WPF) and C# are among my favorites and so I developed Edi

and a few other projects on GitHub. I am normally an algorithms and structure type but WPF has such interesting UI sides that I cannot help myself but get into it.

https://de.linkedin.com/in/dirkbahle

Comments and Discussions

 
Praise5 Stars Pin
skaptor23-Oct-20 13:47
skaptor23-Oct-20 13:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.