I'm going to answer my own question with an explanation of what I needed to do.
It's a long answer as it seems I kept hitting against areas where WPF thought it knew better and would cache. If DataTrigger had a unconditional change, I wouldn't need any of this!
Firstly, let me recap some of the problem again. I have a list-view that can highlight different rows with different styles. Initially, these styles were built-in types, such as Debug and Error. In these cases I could easily latch onto the ViewModel changes of them as DataTriggers in the row-style and make each update immediately.
Once I upgraded to allow user-defined highlighters, I no longer had a property to latch onto (even if I dynamically created them, the style wouldn't know about them).
To get around this, I have implemented a HighlightingService
(this can be discovered at any point by using my ServiceLocator
and asking for a IHightlightingServce
supporting instance). This service implements a number of important properties and methods:
public ObservableCollection<IHighlighter> Highlighters { get; private set; }
public IHighlighterStyle IsHighlighted(ILogEntry logEntry)
{
foreach (IHighlighter highlighter in Highlighters)
{
if ( highlighter.IsMatch(logEntry) )
{
return highlighter.Style;
}
}
return null;
}
Because the Highlighters collection is public accessible, I decided to permit that users of that collection could add/remove entries, negating my need to implement Add/Remove methods. However, because I need to know if the internal IHighlighter
records have changed, in the constructor of the service, I register an observer to its CollectionChanged
property and react to the add/remove items by registering another callback, this allows me to fire a service specific INotifyCollectionChanged
event.
[...]
// Register self as an observer of the collection.
Highlighters.CollectionChanged += HighlightersCollectionChanged;
}
private void HighlightersCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (var newItem in e.NewItems)
{
System.Diagnostics.Debug.Assert(newItem != null);
System.Diagnostics.Debug.Assert(newItem is IHighlighter);
if (e.NewItems != null
&& newItem is IHighlighter
&& newItem is INotifyPropertyChanged)
{
// Register on OnPropertyChanged.
IHighlighter highlighter = newItem as IHighlighter;
Trace.WriteLine(string.Format(
"FilterService detected {0} added to collection and binding to its PropertyChanged event",
highlighter.Name));
(newItem as INotifyPropertyChanged).PropertyChanged += CustomHighlighterPropertyChanged;
}
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (var oldItem in e.OldItems)
{
System.Diagnostics.Debug.Assert(oldItem != null);
System.Diagnostics.Debug.Assert(oldItem is IHighlighter);
if (e.NewItems != null
&& oldItem is IHighlighter
&& oldItem is INotifyPropertyChanged)
{
// Unregister on OnPropertyChanged.
IHighlighter highlighter = oldItem as IHighlighter;
Trace.WriteLine(string.Format(
"HighlightingService detected {0} removed from collection and unbinding from its PropertyChanged event",
highlighter.Name));
(oldItem as INotifyPropertyChanged).PropertyChanged -= CustomHighlighterPropertyChanged;
}
}
}
}
private void CustomHighlighterPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if ( sender is IHighlighter )
{
IHighlighter filter = (sender as IHighlighter);
Trace.WriteLine(string.Format("FilterServer saw some activity on {0} (IsEnabled = {1})",
filter.Name, filter.Enabled));
}
OnPropertyChanged(string.Empty);
}
With all of that, I now know whenever a user has changed a registered highlighter, but it has not fixed the fact that I can't associate a trigger to anything, so I can reflect the changes in the displayed style.
I couldn't find a Xaml only way of sorting this, so I made a custom-control containing my ListView:
public partial class LogMessagesControl : UserControl
{
private IHighlightingService highlight { get; set; }
public LogMessagesControl()
{
InitializeComponent();
highlight = ServiceLocator.Instance.Get<IHighlightingService>();
if (highlight != null && highlight is INotifyPropertyChanged)
{
(highlight as INotifyPropertyChanged).PropertyChanged += (s, e) => UpdateStyles();
}
messages.ItemContainerStyleSelector = new HighlightingSelector();
}
private void UpdateStyles()
{
messages.ItemContainerStyleSelector = null;
messages.ItemContainerStyleSelector = new HighlightingSelector();
}
}
This does a couple of things:
- It assigns a new
HighlightingSelector
to the ItemContainerStyleSelector
(the list-view is called messages
).
- It also registers itself to the
PropertyChanged
event of the HighlighterService which is a ViewModel.
- Upon detecting a change, it replaces the current instance of
HighlightingSelector
on the ItemContainerStyleSelector
(note, it swaps to null first as there is a comment on the web attributed to Bea Costa that this is necessary).
So, now all I need is a HighlightingSelector which takes into account the current highlighting selections (I know that should they change, it will be rebuilt), so I don't need to worry about things too much). The HighlightingSelector
iterates over the registered highlighters and (if they're enabled) registers a style. I cache this a Dictionary
as rebuilding these could be expensive and only get built at the point the user has made a manual interaction, so the increased cost of doing this up front isn't noticeable.
The runtime will make a call to HighlightingSelector.SelectStyle
passing in the record I care about, all I do is return the appropriate style (which was based upon the users original highlighting preferences).
public class HighlightingSelector : StyleSelector
{
private readonly Dictionary<IHighlighter, Style> styles = new Dictionary<IHighlighter, Style>();
public HighlightingSelector()
{
IHighlightingService highlightingService = ServiceLocator.Instance.Get<IHighlightingService>();
if (highlightingService == null) return;
foreach (IHighlighter highlighter in highlightingService.Highlighters)
{
if (highlighter is TypeHighlighter)
{
// No need to create a style if not enabled, should the status of a highlighter
// change, then this collection will be rebuilt.
if (highlighter.Enabled)
{
Style style = new Style(typeof (ListViewItem));
DataTrigger trigger = new DataTrigger();
trigger.Binding = new Binding("Type");
trigger.Value = (highlighter as TypeHighlighter).TypeMatch;
if (highlighter.Style != null)
{
if (highlighter.Style.Background != null)
{
trigger.Setters.Add(new Setter(Control.BackgroundProperty,
new SolidColorBrush((Color) highlighter.Style.Background)));
}
if (highlighter.Style.Foreground != null)
{
trigger.Setters.Add(new Setter(Control.ForegroundProperty,
new SolidColorBrush((Color) highlighter.Style.Foreground)));
}
}
style.Triggers.Add(trigger);
styles[highlighter] = style;
}
}
}
}
public override Style SelectStyle(object item, DependencyObject container)
{
ILogEntry entry = item as ILogEntry;
if (entry != null)
{
foreach (KeyValuePair<IHighlighter, Style> pair in styles)
{
if (pair.Key.IsMatch(entry) && pair.Key.Enabled)
{
return pair.Value;
}
}
}
return base.SelectStyle(item, container);
}
}