Introduction
As a relatively newcomer to WPF, I decided to create a (yet another) Color Picker control, styled after the Visual Studio 2010 Color Picker. It seemed like a good exercise and also practically useful for many applications. In this article, I describe briefly some of the obstacles I stumbled on and a few lessons I learned during the process. The full source code is provided in the link above. The Color Picker control is composed of two parts: the right side is a standard RGB control (with Alpha value) and the left side is an HSV (Hue, Saturation, Value) control. Modification of the RGB control affects the HSV control and vice versa.
Using the Code
You can use the ColorPicker
control directly, or the ColorComboBox
that opens the ColorPicker
in a popup window. In both cases, just embed the control in your window and use the SelectedColor
property to get/set the color.
Also, some of the inner components, such as ColorSlider
, are implemented as custom controls and can be used directly.
TemplateBinding Catch
The ColorSlider
custom control is derived from the Slider
control. Its background is a linear gradient brush between two Dependency Properties: LeftColor
and RightColor
. The natural way to set the background brush is as follows:
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Color="{TemplateBinding LeftColor}/>
<GradientStop Color="{TemplateBinding RightColor}/>
</LinearGradientBrush>
However, this doesn't work. After scratching my head for a while, I recalled that TemplateBinding
is a lightweight version of binding with limitation. Replacing the above code with:
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Color="{Binding RelativeSource={RelativeSource
TemplatedParent}, Path=LeftColor}" Offset="0"/>
<GradientStop Color="{Binding RelativeSource={RelativeSource
TemplatedParent}, Path=RightColor}" Offset="1"/>
</LinearGradientBrush>
did the trick. The moral? When TemplateBinding
doesn't work - replace it with regular binding.
Property Binding Puzzle
The SpectrumSlider
custom control displays the rainbow colors and allows to select a color by moving the thumb. As it is derived from the Slider
control, the position of the thumb is linked to the Value
property. So I added a new dependency property SelectedColor
and bound it in the template to the Value
property using an appropriate converter. Here is the relevant code part:
<Setter Property="SelectedColor">
<Setter.Value>
<Binding
Path="Value"
RelativeSource="{RelativeSource Self}"
Converter="{StaticResource ValueToSelectedColorConverter}" />
</Setter.Value>
</Setter>
This took care of the binding between the two properties. All that was left was to set the SelectedColor
to some initial value and everything should work fine.
Except that it didn't.
The initial color set to the slider did not affect the Value
, and moving the slider thumb did change the Value
but did not change the SelectedColor
. It seemed that the binding did not work. Well, perhaps binding within the control template is problematic, so I tried to bind in the class constructor instead:
public SpectrumSlider()
{
...
Binding binding = new Binding();
binding.Path = new PropertyPath("Value");
binding.RelativeSource = new System.Windows.Data.RelativeSource(RelativeSourceMode.Self);
binding.Mode = BindingMode.TwoWay;
binding.Converter = new ValueToSelectedColorConverter();
BindingOperations.SetBinding(this, SelectedColorProperty, binding);
...
}
That did not work either.
After some more confusion, the puzzle was solved: setting the SelectedColor
to a specific color in the test program removed the binding. I was not aware that binding is managed like a value, and when another value is set to the bound property, the binding is removed. The solution in this case is to link the properties manually in the code-behind (and take care not to create an endless loop).
TextBox Binding On Enter
The ColorPicker
contains TextBox
controls in order to allow quick setting of the RGB and Alpha values. The TextBox
value is bound to the slider Value
property, so setting one affects the other. Surprisingly, when the user fills a value in the TextBox
and presses Enter, the value of the slider is not updated. Only after the focus leaves the TextBox
, by pressing [tab] for example, is the value updated. This is due to the fact that the binding takes effect when the TextBox
loses focus, but not when an Enter is pressed. This is a well known problem of the standard TextBox
binding behavior, and there are a lot of discussions in the related forums.
A standard way to resolve it is to set the UpdateSourceTrigger
property to PropertyChanged
:
<TextBox
Text="{Binding Path=Value, ElementName=PART_Slider, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}"/>
This causes an undesirable effect: each keystroke is reflected immediately in the thumb position. When the user types "123", the thumb jumps to position 1, then to position 12, and finally to position 123.
An interesting suggestion was to use an Attached Property. The general scheme is as follows: the Attached Property hooks to the TextBox
KeyDown
event. When an Enter is pressed, it retrieves the relevant binding and updates the source:
public static class BindingHelper
{
public static readonly DependencyProperty BindOnEnterProperty =
DependencyProperty.RegisterAttached("BindOnEnter",
typeof(DependencyProperty), typeof(BindingHelper),
new UIPropertyMetadata(null, new PropertyChangedCallback(OnBindOnEnterChanged)));
...
private static void OnBindOnEnterChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
UIElement element = d as UIElement;
element.KeyDown += new KeyEventHandler(OnKeyDown);
}
private static void OnKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
UIElement element = e.Source as UIElement;
DependencyProperty property =
(DependencyProperty)element.GetValue(BindOnEnterProperty);
BindingExpression binding =
BindingOperations.GetBindingExpression(element, property);
if (binding != null)
binding.UpdateSource();
}
}
}
The XAML code looks like:
<TextBox
local:BindingHelper.BindOnEnter="TextBox.Text"
Text="{Binding Path=Value, ElementName=PART_Slider, Mode=TwoWay}"/>
This solution has the benefit that the Attached Property can be used for controls other than just TextBox
. However, I chose a much simpler option: create a class derived from TextBox
and override OnKeyDown
:
public class BindOnEnterTextBox : TextBox
{
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (e.Key == Key.Enter)
{
BindingExpression binding =
BindingOperations.GetBindingExpression(this, TextProperty);
if (binding != null)
binding.UpdateSource();
}
}
}
Popup Catch 22
After implementing the ColorPicker
, I wanted to wrap it in a ColorComboBox
that opens the ColorPicker
in a popup window and behaves in a way similar to a regular ComboBox
. When the down arrow is pressed, the popup window opens. When the down arrow is pressed again or mouse is clicked outside the popup, it closes. An obvious solution is to create a class derived from ComboBox
and provide the appropriate template. This seems like too heavy a weapon for the task and creating it from scratch seems easy enough: just create a ToggleButton
, embed the ColorPicker
in a popup window, and bind the ToggleButton
IsChecked
property to the popup IsOpen
property. Here we encounter the popup catch:
If the StaysOpen
property of the popup is set to true
, the popup is opened when the ToggleButton
is clicked once and is closed when the button is clicked again. But it is not closed when any other point is clicked, not even when the top window is dragged or another window is activated. If the StaysOpen
property is set to false
, the behavior changes: when the popup is open and a point outside it is clicked, it is closed due to focus lost, but if the point is within the ToggleButton
button, it is reopened as a result of the button click. One way out of this catch is to disable the ToggleButton
when the popup is opened and re-enable it after the popup is closed. That way, when clicking on it, the popup is closed, but the ToggleButton
does not fire the click event since it is disabled.
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.