Sunday, January 6, 2013

Custom PropertyGrid control

Goal

WPF contains not very much default controls. But it is not difficult to implement them by yourself.

In one day i get request to implement something like PropertyGrid - it should contain property groups and search bar. I was surprised how well this problem could be solved.

In this post i show how to implement something like this (every control uses it's default style):

Planning

Property grid would be implemented with UserControl. Here would be 3 main elements: search bar, property group and properties. CustomPropertyGrid would use only two property editor - for strings and booleans.

Most of the logic (providing update mechanism and searching) would contained in ModelView part. View part consist of UserControl and DataTemplates. View would know about changes through INotifyPropertyChanged interface.

Implementation

Custom PropertyGrid inhereted from UserControl and contains TextBox and ItemsControl binded to DataContext.

  
<UserControl ... >
    <UserControl.Resources>
        ...
    </UserControl.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        
        <customPropertyGridDemo:WatermarkTextBox Text="{Binding SearchString, UpdateSourceTrigger=PropertyChanged}" />
        <ScrollViewer Grid.Row="1" >
            <ItemsControl ItemsSource="{Binding Groups}" >
                <ItemsControl.ItemsPanel>
                    <!--It is not necessary, because StackPanel is default panel. -->
                    <ItemsPanelTemplate>
                        <StackPanel />
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
            </ItemsControl>
        </ScrollViewer>
    </Grid>
</UserControl>

DataContext for PropertyGrid contains only search string property and collection of PropertyGroups.

 
public class PropertyGridVM : BaseVM
{
 public PropertyGridVM()
 {
  Groups = new ObservableCollection();
 }

 public ObservableCollection Groups { get; set; }

 private string searchString;
 public string SearchString
 {
  get { return searchString; }
  set
  {
   if (searchString == value) return;
   searchString = value;
   RaisePropertyChanged();
   UpdateFiltering();
  }
 }

 private void UpdateFiltering()
 {
  if (string.IsNullOrWhiteSpace(SearchString))
  {
   foreach (var propertyGroup in Groups)
   {
    propertyGroup.ClearFiltering();
   }
  }
  else
  {
   foreach (var propertyGroup in Groups)
   {
    propertyGroup.ApplyFiltering(SearchString);
   }
  }
 }
}

Base class BaseVM contains only INotifyPropertyChanged interface implementation. So on every change we updating Filtering. Logic for filtering transmitted further to PropertyGroup:

 
public class PropertyGroup : BaseVM
{
 public ObservableCollection Properties { get; set; }
 
 public string Name {get {...} set {...}}
 public bool IsVisible {get {...} set {...}}

 public void ApplyFiltering(string filterString)
 {
  foreach (var property in Properties)
  {
   property.ApplyFiltering(filterString);
  }
  IsVisible = Properties.Any(prop => prop.IsVisible);
 }

 public void ClearFiltering()
 {
  foreach (var property in Properties)
  {
   property.ClearFiltering();
  }
  IsVisible = true;
 }

 public ObservableCollection Properties { get; set; }
}

On every filter change - group updates its property IsVisible. Same thing done in every property only logic a little bit different. Through binding IsVisible updates PropertyGrid visual state. For example there are two DataTemplates - one for PropertyGroup and another for StringProperty.

<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>

<DataTemplate DataType="{x:Type modelView:PropertyGroup}">
 <Expander IsExpanded="True" Header="{Binding Name}" Visibility="{Binding IsVisible, Converter={StaticResource BooleanToVisibilityConverter}}">
  <ItemsControl ItemsSource="{Binding Properties}" />
 </Expander>
</DataTemplate>

<DataTemplate DataType="{x:Type modelView:StringProperty}">
 <Grid Visibility="{Binding IsVisible, Converter={StaticResource BooleanToVisibilityConverter}}">
  <Grid.ColumnDefinitions>
   <ColumnDefinition Width="0.4*" MinWidth="100" />
   <ColumnDefinition Width="0.6*" MinWidth="100" />
  </Grid.ColumnDefinitions>
  
  <Label Target="{Binding ElementName=valueEditor}" Content="{Binding Name}" HorizontalAlignment="Right" VerticalAlignment="Center" />
  
  <!--This is hack, because TextBox by default tries to get enough space for its content -->
  <Border x:Name="bd" Grid.Column="1" />
  <TextBox x:Name="valueEditor" Grid.Column="1" Text="{Binding Value}" Width="{Binding ElementName=bd, Path=ActualWidth}" VerticalAlignment="Center" />
 </Grid>
</DataTemplate>

Every DataTemplate for Property contains Label and value editor, in this case TextBox. For any other type of editors you should implement it's DataTemplate by your own.

To use this control we would create TestClass with properties we would like to edit:

 
public class TestClass
{
 [Browsable(true)]
 [Category("Group1")]
 [DisplayName("Property1")]
 public string Group1Property1 {get; set;}
 
 // ... other properties.
}

Every editable property we mark by these attributes. Through reflection we create PropertyProxy objects. Its used by Property class to get and set object's editalbe properties.

 
public class PropertyProxy
{
 public object GetValue()
 { ... }

 public void SetValue(object value)
 { ... }
}

There some parts for code improvements, but hope general idea is clear. If you want to make it more like VisualStudio PropertyGrid - you should update Expander Style and all DataTemplates for editors, but logic would be the same.

Link for project sources.

Saturday, January 5, 2013

Simple Watermark control

Goal

There is no watermark in standard WPF TextBox but it is simple to create one.
Our goal to get the result like this:

Some concepts about control

To get enough functionality we will expand TextBox (for our goal we can use class UserControl and in many cases we should do this, but we want our control behave and have the same properties like regular TextBox).

Our WatermarkTextBox would have two additional properties: text property for watermark content and boolean property for watermark visibility (true if control is not focused and TextBox.Text property is empty).

In WPF every control should provide it's look through classes Style and ContentTemplate in xaml theme file (also you could do this by code - but by xaml it is just easier).

Implementation

In Visual Studio 2010/2012 to implement custom control best of all to use standard Template WPF CustomControl. This template automatically adds default resource file Themes/Generic.xaml and special attribute ThemeInfoAttribute to AssemblyInfo.cs file pointing wpf to use this theme file.

New properties Watermark and IsWatermarkVisible stated as DependencyProperty (it is default way in WPF to create property that participate in control visualization and animation):

public class WatermarkTextBox : TextBox
{
    // DependencyProperty of watermark content. You can change this to UI content, so you can use any template.
    public static readonly DependencyProperty WatermarkProperty =
  DependencyProperty.Register("Watermark", typeof (string), typeof (WatermarkTextBox), new PropertyMetadata("Enter text..."));
    public string Watermark
    {
  get { return (string) GetValue(WatermarkProperty); }
        set { SetValue(WatermarkProperty, value); }
    }

    // DependancyProperty of Watermark visibility.
    public static readonly DependencyProperty IsWatermarkVisibleProperty =
  DependencyProperty.Register("IsWatermarkVisible", typeof (bool), typeof (WatermarkTextBox), new PropertyMetadata(true));
    public bool IsWatermarkVisible
    {
  get { return (bool) GetValue(IsWatermarkVisibleProperty); }
        set { SetValue(IsWatermarkVisibleProperty, value); }
    }
}

To update IsWatermarkVisible properties we should track for TextBox.Text property and focus changes. Doing like so:

protected override void OnTextChanged(TextChangedEventArgs e)
{
    base.OnTextChanged(e);
    UpdateWatermarkVisibility();
}

protected override void OnGotKeyboardFocus(System.Windows.Input.KeyboardFocusChangedEventArgs e)
{
    base.OnGotKeyboardFocus(e);
    UpdateWatermarkVisibility();
}

protected override void OnLostKeyboardFocus(System.Windows.Input.KeyboardFocusChangedEventArgs e)
{
    base.OnLostKeyboardFocus(e);
    UpdateWatermarkVisibility();
}

/// 
/// The only place where visibility can be changed. 
/// This logic can be placed in xaml Style, but it gets a little bit complex - so it may be more desirable to keep logic in code 
/// 
private void UpdateWatermarkVisibility()
{
    IsWatermarkVisible = !IsKeyboardFocused && string.IsNullOrEmpty(Text);
}

Now we add watermark visual element. To do this we provide own ContentTemplate and Style in Themes/Generic.xaml:

    
    <Style TargetType="{x:Type wd:WatermarkTextBox}" >
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type wd:WatermarkTextBox}">
                    <Border Name="Bd"
                            Background="{TemplateBinding Background}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            BorderBrush="{TemplateBinding BorderBrush}">
                        <Grid>
                            <ScrollViewer x:Name="PART_ContentHost"/>
                            <!--Added watermark textblock-->
                            <TextBlock x:Name="Watermark" Text="{TemplateBinding Watermark}" Visibility="Collapsed" Foreground="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
                        </Grid>
                    </Border>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsEnabled"
                                 Value="false">
                            <Setter TargetName="Bd"
                                    Property="Background"
                                    Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
                            <Setter Property="Foreground"
                                    Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                        </Trigger>
                        <!-- Setting visibility of watermark with trigger -->
                        <Trigger Property="IsWatermarkVisible" 
                                 Value="True">
                            <Setter TargetName="Watermark" 
                                    Property="Visibility" 
                                    Value="Visible"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

It's slightly modified style of classic theme.

Sources for project

NOTE: Default Style key usually stated in Static constructor:

        
static WatermarkTextBox()
{
 // Setting default style key. This used by wpf to get right style from resources.
    DefaultStyleKeyProperty.OverrideMetadata(typeof(WatermarkTextBox), new FrameworkPropertyMetadata(typeof(WatermarkTextBox)));
}