tags:

views:

1302

answers:

3

I have an ObservableCollection bound to a list box and have a highlight mechanism set up with data triggers, when I had a simple set of highlighters (debug, warning, etc) I could simply enumerate the style with several data-triggers bound to the view model that exposes those options.

I have now upgraded the system to support multiple userdefined highlighters which expose themselves with IsHighlighted(xxx) methods (not properties).

How can I make the the ListView aware that the visual state (style's datatrigger) has changed? Is there a "refreshed" event I can fire and catch in a datatrigger?

Update I have a DataTrigger mapped to an exposed property Active which simply returns a value of true, but despite that there is no update:

<DataTrigger Binding="{Binding Highlight.Active}"
             Value="true">
    <Setter Property="Background"
            Value="{Binding Type, Converter={StaticResource typeToBackgroundConverter}}" />
    <Setter Property="Foreground"
            Value="{Binding Type, Converter={StaticResource typeToForegroundConverter}}" />
 </DataTrigger>
+1  A: 

When the condition of a DataTrigger changes, this should automatically cause the parent UI element to refresh.

A couple of things to check: 1. The input data of the trigger is actually changing as you expect it to. 2. The input data of the trigger binds to a dependency property. Otherwise, you will never know when the value updates.

If you showed us the appropiate parts of your XAML, that would help a great deal.

Noldorin
The datatriggers work correctly for the INotifyValueChanged announced properties, but for the user defined ones, I have no property. The current implementation uses a IValueConverter get the appropriate fore/back-colour irrespective to them being built-in or user defined. But the ones already shown refresh their colour (content changes) because it isn't bound to anything. If I keep the old button state binding in place and toggle one of the built-in states (e.g. Warning) then it refreshes fine.
Ray Hayes
+1  A: 

If you just want to set the colour of the item somehow, you could write a converter that does what you want:

<Thing Background="{Binding Converter={StaticResource MyItemColorConverter}}" />

In this case, the converter could call your IsHighlighted(xxx) method and return the appropriate colour for the Thing.

If you want to set more than one property, you could use multiple converters, but the idea starts to fall apart at some point.

Alternatively, you could use a converter on your DataBinding to determine whether the item in question falls into a certain category and then apply setters. It depends upon what you need!

EDIT

I have just re-read your question and realised I'm off the mark. Whoops.

I believe you can just raise INotifyPropertyChanged.PropertyChanged with a PropertyChangedEventArgs that uses string.Empty, and that forces the WPF binding infrastructure to refresh all bindings. Have you tried that?

Drew Noakes
But I have nothing to bind to. There could be multiple highlights and none of the user-defined ones are exposed as a bindable property. I guess I could make a single bindable "general" property and raise a PropertyChanged event on that - have it combined with some form of converter that takes the "ItemSource" item as a parameter.
Ray Hayes
If you're binding using an `IValueConverter` as you describe in a comment elsewhere, then you're binding to something. In the example above, the absent `Path` attribute mean 'bind to the item itself'. Have you tried seeing what happens when that item fires `PropertyChanged` as I describe?
Drew Noakes
In my record, I'm binding to the "type" field - this doesn't change. The ValueConverter uses a ServiceLocator to look up what the colours should be for that type. However, because the enabling/disabling of highlighting isn't being seen, no update is happening for the style to datatrigger off.
Ray Hayes
I'm considering making the Highlighter service a bindable property and in the event of any change, firing the `PropertyChanged` event and use a MultiDataTrigger instead.
Ray Hayes
No joy with an empty string or with a fake property, it seems that WPF knows what the old state was and detects that no change was made. Because the fake property I added always returns true and I must supply a match value set to something.....
Ray Hayes
A: 

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:

  1. It assigns a new HighlightingSelector to the ItemContainerStyleSelector (the list-view is called messages).
  2. It also registers itself to the PropertyChanged event of the HighlighterService which is a ViewModel.
  3. 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);
    }
}
Ray Hayes