Я собираюсь ответить на свой вопрос с объяснением того, что мне нужно было сделать.
Это длинный ответ, так как кажется, что я продолжал сталкиваться с областями, где WPF считал, что знает лучше и будет кешировать. Если бы DataTrigger имел безусловное изменение, мне бы это не понадобилось!
Во-первых, позвольте мне еще раз повторить некоторые проблемы. У меня есть ListView, который может выделить разные строки с разными стилями. Первоначально эти стили были встроенными типами, такими как Debug и Error. В этих случаях я мог бы легко зафиксировать изменения их ViewModel как DataTriggers в стиле строки и немедленно выполнить каждое обновление.
После того, как я обновил, чтобы разрешить пользовательские маркеры, у меня больше не было свойства для привязки (даже если я их динамически создавал, стиль не знал бы о них).
Чтобы обойти это, я реализовал HighlightingService
(это можно обнаружить в любой момент, используя мой ServiceLocator
и запросив экземпляр поддержки IHightlightingServce
). Этот сервис реализует ряд важных свойств и методов:
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;
}
Поскольку коллекция Highlighters общедоступна, я решил разрешить пользователям этой коллекции добавлять / удалять записи, что сводит на нет мою потребность в реализации методов Add / Remove. Однако, поскольку мне нужно знать, изменились ли внутренние записи IHighlighter
, в конструкторе службы я регистрирую наблюдателя на его свойство CollectionChanged
и реагирую на добавление / удаление элементов, регистрируя другой обратный вызов, это позволяет мне для запуска специфичного для службы INotifyCollectionChanged
события.
[...]
// 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);
}
При всем этом я теперь знаю, когда пользователь изменил зарегистрированный маркер, но он не зафиксировал тот факт, что я не могу связать триггер с чем-либо, поэтому я могу отражать изменения в отображаемом стиле.
Мне не удалось найти только способ сортировки в Xaml, поэтому я создал пользовательский элемент управления, содержащий мой 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();
}
}
Это делает пару вещей:
- Назначает новый
HighlightingSelector
для ItemContainerStyleSelector
(ListView называется messages
).
- Он также регистрируется на событие
PropertyChanged
HighlighterService, которое является ViewModel.
- При обнаружении изменения он заменяет текущий экземпляр
HighlightingSelector
на ItemContainerStyleSelector
(обратите внимание, что сначала он обнуляется, поскольку в сети есть комментарий, приписываемый Bea Costa, что это необходимо).
Итак, теперь все, что мне нужно, это HighlightingSelector, который учитывает текущий выделения выделения (я знаю, что если они изменятся, он будет перестроен), поэтому мне не нужно беспокоиться о вещах перебор). HighlightingSelector
перебирает зарегистрированные маркеры и (если они включены) регистрирует стиль. Я кеширую это в Dictionary
, так как их восстановление может быть дорогостоящим, и, поскольку они создаются только в тот момент, когда пользователь выполнил ручное взаимодействие, увеличение затрат на это сразу не заметно.
Среда выполнения вызовет HighlightingSelector.SelectStyle
с передачей нужной мне записи, все, что я делаю, это возвращаю соответствующий стиль (который был основан на первоначальных настройках подсветки пользователя).
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);
}
}