Introduction
Although I use WPF as UI technology I think it is easy to implement this approach on any desktop UI technology. I beleive it will be helpful for applications where one should enter file system paths to files and folders.
Background
One day I decided to create a program for editing environment variable PATH. I think you know it: long and hard to read list of semicolon separated paths in file system. It is hard to read, hard to modify and hard to understand if some parts of PATH do not exists anymore. And it is easy to make some typo while modifying it.
So I created a simple WPF application which breaks PATH to separate parts, displays them as a list, highlights those parts which do not really exist in file system and allows to create/modify/delete separate parts:
And of course this application has a textbox to enter and change parts of PATH variable (highlighted by red borders on the picture). I desided that it is good to implement some autocomplete functionality for this textbox. If you type some path in Windows Command Prompt you can press Tab and the program will try to complete current part of your path for you. I wanted something similar but in WPF TextBox.
Requirements
Certainly there are some changes from command line tool. First of all I did not want to use Tab key. In desktop UI technologies Tab is used to move to the next/previous control on a window form. So I desided that my TextBox should suggest completion of my path by automatically adding some selected text to the end of text in the TextBox.
In this case if I press any characted key it will replace the selected text and I can continue typing without distraction. But if I want to accept the suggestion then I can just press End key on the keyboard.
Second thing I wanted to consider is that if I type something in the middle of text (not in the end) then my autocomplete functionality should do nothing. Suggestions should only appear if I type something in the end of text.
And finally there is one more thing related to the specifics of PATH variable. Parts of this variable can contain references to other environment variables like %SystemRoot%\System32
. I wanted my TextBox to autocomplete names of these environment variables as well.
Now lets dive into the code.
Code of TextBox control
First of all I decided to create a descendant of TextBox class that will implement functionality allowing to plug in any AutoComplete provider:
public class AutoCompleteTextBox : System.Windows.Controls.TextBox
I decided that when this TextBox requires autocompletion of its text it will call associated command. So I created a dependency property AutoCompleteCommand
:
public static ICommand GetAutoCompleteCommand(DependencyObject obj)
{ return (ICommand)obj.GetValue(DialogResultProperty); }
public static void SetAutoCompleteCommand(DependencyObject obj, ICommand value)
{ obj.SetValue(DialogResultProperty, value); }
public static readonly DependencyProperty DialogResultProperty = DependencyProperty.Register("AutoCompleteCommand",
typeof(ICommand),
typeof(AutoCompleteTextBox));
This command is called from OnKeyUp
event handler:
protected override void OnKeyUp(KeyEventArgs e)
{
base.OnKeyUp(e);
if (CaretIndex == Text.Length)
{
if (e.Key == Key.Delete || e.Key == Key.Back)
{ return; }
var autoCompleteCommand = GetAutoCompleteCommand(this);
if (autoCompleteCommand != null && autoCompleteCommand.CanExecute(this))
{
autoCompleteCommand.Execute(this);
}
}
}
As you can see from condition of IF statement the autocomplete command is called only if the caret is at the end of text. Also there is a special handling of Delete and Backspace keys. It prevents autocompletion after removing parts of text in the end. The autocomplete command always gets the TextBox as the argument as we need to be able to modify the text of this TextBox from the command.
One last piece of code is a method which adds some selected text into the end of TextBox:
public void AppendSelectedText(string textToAppend)
{
if (string.IsNullOrEmpty(textToAppend))
{ return; }
var currentEndPosition = Text.Length;
Text += textToAppend;
CaretIndex = currentEndPosition;
SelectionStart = currentEndPosition;
SelectionLength = textToAppend.Length;
}
And here is how one can use this new TextBox in XAML:
<uc:AutoCompleteTextBox AutoCompleteCommand="{Binding AutoComplete}" />
The code of the autocomplete command is also very simple:
public ICommand AutoComplete
{
get
{
return new DelegateCommand(arg =>
{
var textBox = (AutoCompleteTextBox) arg;
var autoCompleteProvider = AutoCompleteProviderFactory.GetAutoCompleteProvider(textBox.Text);
textBox.AppendSelectedText(autoCompleteProvider.GetAutoCompleteText());
});
}
}
All magic happens inside autocomplete providers.
Code of autocomplete providers
As you probably understand I wanted to have two autocomplete providers: one to complete file system paths and one to complete names of environment variables. Class AutoCompleteProviderFactory
chooses which provider should be used:
internal class AutoCompleteProviderFactory
{
private static readonly IAutoCompleteProvider Empty = new EmptyAutoCompleteProvider();
public static IAutoCompleteProvider GetAutoCompleteProvider(string text)
{
if (string.IsNullOrEmpty(text))
{ return Empty; }
var indexOfSeparator = text.LastIndexOf(Path.DirectorySeparatorChar);
string prefix = text.Substring(0, indexOfSeparator + 1);
string textToAppend = text.Substring(indexOfSeparator + 1);
if (textToAppend.StartsWith("%"))
return new EnvironmentVariableAutoCompleteProvider(textToAppend);
if (!string.IsNullOrEmpty(prefix))
return new DirectoryAutoCompleteProvider(prefix, textToAppend);
return Empty;
}
}
Depending on the text of TextBox it returns specific instance of IAutoCompleteProvider
interface:
internal interface IAutoCompleteProvider
{
string GetAutoCompleteText();
}
Here EmptyAutoCompleteProvider
is a simple implementation which always return empty string from GetAutoCompleteText
method.
Lets consider two other providers.
internal class EnvironmentVariableAutoCompleteProvider : IAutoCompleteProvider
{
private static readonly string[] EnvironmentVariables = Environment.GetEnvironmentVariables().Keys.OfType<string>().ToArray();
private readonly string _variableNamePrefix;
public EnvironmentVariableAutoCompleteProvider(string textToAppend)
{
if (string.IsNullOrEmpty(textToAppend)) throw new ArgumentNullException("textToAppend");
_variableNamePrefix = textToAppend.Substring(1);
}
public string GetAutoCompleteText()
{
if (string.IsNullOrEmpty(_variableNamePrefix))
return string.Empty;
var variant = EnvironmentVariables
.FirstOrDefault(v => v.StartsWith(_variableNamePrefix, StringComparison.OrdinalIgnoreCase)) ?? string.Empty;
if (variant == string.Empty)
return string.Empty;
return variant.Substring(_variableNamePrefix.Length) + "%";
}
}
EnvironmentVariableAutoCompleteProvider
completes names of environment variables. For the sake of performance the list of names of environment variables is obtained only once and stored in a static field.
internal class DirectoryAutoCompleteProvider : IAutoCompleteProvider
{
private static string _currentDirectory;
private static string[] _currentSubDirectories;
private readonly bool _directoryExists;
private readonly string _textToAppend;
public DirectoryAutoCompleteProvider(string prefix, string textToAppend)
{
_textToAppend = textToAppend;
_directoryExists = prefix.ExpandedDirectoryExists();
if (_directoryExists)
{
RefreshSubdirectories(prefix);
}
}
private void RefreshSubdirectories(string prefix)
{
string directory = Environment.ExpandEnvironmentVariables(prefix);
if (directory.Equals(_currentDirectory, StringComparison.OrdinalIgnoreCase))
return;
_currentDirectory = directory;
_currentSubDirectories = Directory.GetDirectories(directory)
.Select(Path.GetFileName).ToArray();
}
public string GetAutoCompleteText()
{
if (_directoryExists == false || string.IsNullOrEmpty(_textToAppend))
return string.Empty;
var variant = _currentSubDirectories
.FirstOrDefault(d => d.StartsWith(_textToAppend, StringComparison.OrdinalIgnoreCase)) ?? string.Empty;
if (variant == string.Empty)
return string.Empty;
return variant.Substring(_textToAppend.Length) + Path.DirectorySeparatorChar;
}
}
DirectoryAutoCompleteProvider
completes paths to directories. During this process it respects the fact that part of these paths can be represented by environment variables (like %SystemRoot%\System32
). First of all it checks if directory which path is completely entered into the TextBox exists. For this purpose it uses ExpandedDirectoryExists
method:
public static bool ExpandedDirectoryExists(this string str)
{
if (string.IsNullOrWhiteSpace(str))
return false;
try
{
return Directory.Exists(Environment.ExpandEnvironmentVariables(str));
}
catch
{
return false;
}
}
If the directory exists the provider gets names of all subdirectories inside it, but only once per directory. The rest of the code is rather simple.
I hope this simple example will help you to build your applications for work with file system and to create autocomplete controls for different cases. You can get complete code of my application from https://github.com/yakimovim/PathEditor.
History
Revision |
Date |
Comment |
1.0 |
25.09.2014 |
Initial revision |
1.1 |
26.09.2014 |
Archive of source code is added |