I'm learning how to write WPF custom controls. My example, NameLister
, exposes a single DependencyProperty, Names
, of type ObservableCollection<string>
, and displays all the names concatenated together into a single string in a TextBlock
.
To test my control, I have a TestApp
which binds a collection of three strings: "Larry", "Curly", and "Moe" to the Names
property of my control. It then has a repeating timer which adds "Me" strings to its collection.
There are a few things I'm puzzled about:
- This sure seems like a lot of code just for one
DependencyProperty
to which someone can bind anObservableCollection
. - Should I be using
ObservableCollection
,IEnumerable
, orIList
as the type for myNames
DependencyProperty
? - I made my custom control implement INotifyPropertyChanged. I did this because I use a normal property,
DisplayString
, bound to theTextBlock
'sText
property in the XAML. However, I've heard this is a bad idea (because my custom control is a view, not a view-model?)
TestApp
MainWindow.xaml
<Window x:Class="TestApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:sdwc="clr-namespace:NameListerWpfControl;assembly=NameListerWpfControl"
xmlns:local="clr-namespace:TestApp"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800" BorderBrush="#FF0010FF">
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
<Grid>
<sdwc:NameLister Background="Red" Names="{Binding Stooges}" />
</Grid>
</Window>
ViewModel.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace TestApp
{
public class ViewModel : INotifyPropertyChanged
{
public ObservableCollection<string> Stooges { get; set; } = new ObservableCollection<string> {"Larry", "Curly", "Moe"};
public ViewModel()
{
var dispatcherTimer = new System.Windows.Threading.DispatcherTimer
{
Interval = new TimeSpan(0, 0, 2)
};
dispatcherTimer.Tick += delegate
{
Stooges.Add("Me");
RaisePropertyChanged(nameof(Stooges));
};
dispatcherTimer.Start();
}
/// <summary>
/// The property changed event
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raises the property changed event
/// </summary>
/// <param name="property">The property name</param>
public void RaisePropertyChanged(string property)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
}
}
NameListerWpfControl
Themes/Generic.xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:NameListerWpfControl">
<Style TargetType="{x:Type local:NameLister}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:NameLister}">
<TextBlock Text="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type local:NameLister}},
Path=DisplayString}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
NameLister.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace NameListerWpfControl
{
/// <summary>
/// Lists names
/// </summary>
public class NameLister : Control, INotifyPropertyChanged
{
static NameLister()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(NameLister),
new FrameworkPropertyMetadata(typeof(NameLister)));
}
/// <summary>
/// The names displayed as a single string
/// </summary>
public string DisplayString => string.Join(", ", Names);
/// <summary>
/// Identifies the Names dependency property.
/// </summary>
public static readonly DependencyProperty NamesProperty =
DependencyProperty.Register(
"Names",
typeof(ObservableCollection<string>),
typeof(NameLister),
new FrameworkPropertyMetadata(
new ObservableCollection<string> (),
OnNamesChanged,
CoerceStrings));
/// <summary>
/// Gets or sets the value assigned to the control.
/// </summary>
public ObservableCollection<string> Names
{
get => (ObservableCollection<string>)GetValue(NamesProperty);
set => SetValue(NamesProperty, value);
}
private static object CoerceStrings(DependencyObject element, object value)
{
var newValue = (ObservableCollection<string>)value;
// TODO: Actually do some coercion.
return newValue;
}
private static void OnNamesChanged(DependencyObject obj,
DependencyPropertyChangedEventArgs args)
{
var control = (NameLister)obj;
control.RaisePropertyChanged(nameof(DisplayString));
var action = new NotifyCollectionChangedEventHandler(
(o, args2) =>
{
if (!(obj is NameLister lister)) return;
lister.RaisePropertyChanged(nameof(DisplayString));
});
if (args.OldValue != null)
{
var coll = (INotifyCollectionChanged)args.OldValue;
// Unsubscribe from CollectionChanged on the old collection
coll.CollectionChanged -= action;
}
if (args.NewValue != null)
{
var coll = (INotifyCollectionChanged)args.NewValue;
// Subscribe to CollectionChanged on the new collection
coll.CollectionChanged += action;
}
var e = new RoutedPropertyChangedEventArgs<ObservableCollection<string>>(
(ObservableCollection<string>)args.OldValue,
(ObservableCollection<string>)args.NewValue,
NamesChangedEvent);
control.OnNamesChanged(e);
}
/// <summary>
/// Identifies the NamesChanged routed event.
/// </summary>
public static readonly RoutedEvent NamesChangedEvent =
EventManager.RegisterRoutedEvent(
"NamesChanged",
RoutingStrategy.Bubble,
typeof(RoutedPropertyChangedEventHandler<ObservableCollection<string>>),
typeof(NameLister));
/// <summary>
/// Occurs when the Names property changes.
/// </summary>
public event RoutedPropertyChangedEventHandler<ObservableCollection<string>> NamesChanged
{
add => AddHandler(NamesChangedEvent, value);
remove => RemoveHandler(NamesChangedEvent, value);
}
/// <summary>
/// Raises the NamesChanged event.
/// </summary>
/// <param name="args">Arguments associated with the NamesChanged event.</param>
protected virtual void OnNamesChanged(RoutedPropertyChangedEventArgs<ObservableCollection<string>> args)
{
RaiseEvent(args);
}
/// <summary>
/// The property changed event
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raises the property changed event
/// </summary>
/// <param name="property">The property name</param>
public void RaisePropertyChanged(string property)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
}
}