在构造函数中,当我将两个项目添加到绑定到TextBlock的附加属性的ObservableCollection中时,将更新附加属性。但是,当我稍后通过另一种方法将项目添加到相同的ObservableCollection时,附加属性不会更新。
XAML :
<TextBlock local:TextBlockExtensions.BindableInlines="{Binding StatusInlines}" />
TextBlockExtensions.cs :
public class TextBlockExtensions : BaseViewModel
{
public static IEnumerable<Inline> GetBindableInlines(DependencyObject obj)
{
return (IEnumerable<Inline>)obj.GetValue(BindableInlinesProperty);
}
public static void SetBindableInlines(DependencyObject obj, IEnumerable<Inline> value)
{
obj.SetValue(BindableInlinesProperty, value);
}
public static readonly DependencyProperty BindableInlinesProperty =
DependencyProperty.RegisterAttached("BindableInlines", typeof(IEnumerable<Inline>), typeof(TextBlockExtensions), new PropertyMetadata(null, OnBindableInlinesChanged));
private static void OnBindableInlinesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var Target = d as TextBlock;
if (Target != null)
{
Target.Inlines.Clear();
Target.Inlines.AddRange((System.Collections.IEnumerable)e.NewValue);
}
}
}
ViewModel :
public ObservableCollection<Inline> StatusInlines { get; set; } = new ObservableCollection<Inline>();
...
// works in the constructor (called twice)
StatusInlines.Add(new Run($"{ text }{ Environment.NewLine }") { ToolTip = "tooltip" });
...
// does not work later in other methods
StatusInlines.Add(new Run($"{ text }{ Environment.NewLine }") { ToolTip = "tooltip" });
BaseViewModel.cs (由angelsix提供):
public class BaseViewModel : INotifyPropertyChanged
{
#region Protected Members
/// <summary>
/// A global lock for property checks so prevent locking on different instances of expressions.
/// Considering how fast this check will always be it isn't an issue to globally lock all callers.
/// </summary>
protected object mPropertyValueCheckLock = new object();
#endregion
/// <summary>
/// The event that is fired when any child property changes its value
/// </summary>
public event PropertyChangedEventHandler PropertyChanged = (sender, e) => { };
/// <summary>
/// Call this to fire a <see cref="PropertyChanged"/> event
/// </summary>
/// <param name="name"></param>
public void OnPropertyChanged(string name)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
#region Command Helpers
/// <summary>
/// Runs a command if the updating flag is not set.
/// If the flag is true (indicating the function is already running) then the action is not run.
/// If the flag is false (indicating no running function) then the action is run.
/// Once the action is finished if it was run, then the flag is reset to false
/// </summary>
/// <param name="updatingFlag">The boolean property flag defining if the command is already running</param>
/// <param name="action">The action to run if the command is not already running</param>
/// <returns></returns>
protected async Task RunCommandAsync(Expression<Func<bool>> updatingFlag, Func<Task> action)
{
// Lock to ensure single access to check
lock (mPropertyValueCheckLock)
{
// Check if the flag property is true (meaning the function is already running)
if (updatingFlag.GetPropertyValue())
return;
// Set the property flag to true to indicate we are running
updatingFlag.SetPropertyValue(true);
}
try
{
// Run the passed in action
await action();
}
finally
{
// Set the property flag back to false now it's finished
updatingFlag.SetPropertyValue(false);
}
}
/// <summary>
/// Runs a command if the updating flag is not set.
/// If the flag is true (indicating the function is already running) then the action is not run.
/// If the flag is false (indicating no running function) then the action is run.
/// Once the action is finished if it was run, then the flag is reset to false
/// </summary>
/// <param name="updatingFlag">The boolean property flag defining if the command is already running</param>
/// <param name="action">The action to run if the command is not already running</param>
/// <typeparam name="T">The type the action returns</param>
/// <returns></returns>
protected async Task<T> RunCommandAsync<T>(Expression<Func<bool>> updatingFlag, Func<Task<T>> action, T defaultValue = default(T))
{
// Lock to ensure single access to check
lock (mPropertyValueCheckLock)
{
// Check if the flag property is true (meaning the function is already running)
if (updatingFlag.GetPropertyValue())
return defaultValue;
// Set the property flag to true to indicate we are running
updatingFlag.SetPropertyValue(true);
}
try
{
// Run the passed in action
return await action();
}
finally
{
// Set the property flag back to false now it's finished
updatingFlag.SetPropertyValue(false);
}
}
#endregion
}
答案 0 :(得分:1)
现在,您只需设置一次TextBlock
内联。
您忘记订阅INotifyCollectionChanged.CollectionChanged
事件并处理已更改的项目。
在绑定源实现TextBlockExtensions
并相应处理更改的情况下,以下INotifyCollectionChanged.CollectionChanged
的重构版本会监听INotifyCollectionChanged
:
TextBlockExtensions.cs
public class TextBlockExtensions : BaseViewModel
{
public static readonly DependencyProperty BindableInlinesProperty =
DependencyProperty.RegisterAttached(
"BindableInlines",
typeof(IEnumerable<Inline>),
typeof(TextBlockExtensions),
new PropertyMetadata(default(IEnumerable<Inline>), OnBindableInlinesChanged));
public static IEnumerable<Inline> GetBindableInlines(DependencyObject obj) =>
(IEnumerable<Inline>) obj.GetValue(BindableInlinesProperty);
public static void SetBindableInlines(DependencyObject obj, IEnumerable<Inline> value) =>
obj.SetValue(BindableInlinesProperty, value);
private static Dictionary<INotifyCollectionChanged, IList<WeakReference<TextBlock>>> CollectionToTextBlockMap { get; set; }
static TextBlockExtensions()
{
TextBlockExtensions.CollectionToTextBlockMap =
new Dictionary<INotifyCollectionChanged, IList<WeakReference<TextBlock>>>();
}
private static void OnBindableInlinesChanged(DependencyObject attachingElement, DependencyPropertyChangedEventArgs e)
{
if (!(attachingElement is TextBlock textBlock))
{
throw new ArgumentException("Attaching element must be of type 'TextBlock'.");
}
TextBlockExtensions.Cleanup(textBlock, e.OldValue);
if (!(e.NewValue is IEnumerable<Inline> inlineElements))
{
return;
}
textBlock.Inlines.AddRange(inlineElements);
if (inlineElements is INotifyCollectionChanged observableCollection)
{
ObserveCollectionChanges(observableCollection, textBlock);
}
}
private static void Cleanup(TextBlock textBlock, object oldCollection)
{
textBlock.Inlines.Clear();
if (oldCollection is INotifyCollectionChanged oldObservableCollection)
{
oldObservableCollection.CollectionChanged -= TextBlockExtensions.UpdateTextBlockOnCollectionChanged;
TextBlockExtensions.CollectionToTextBlockMap.Remove(oldObservableCollection);
}
}
private static void ObserveCollectionChanges(INotifyCollectionChanged observableCollection, TextBlock textBlock)
{
if (TextBlockExtensions.CollectionToTextBlockMap.TryGetValue(
observableCollection,
out IList<WeakReference<TextBlock>> boundTextBoxes))
{
boundTextBoxes.Add(new WeakReference<TextBlock>(textBlock));
}
else
{
observableCollection.CollectionChanged += TextBlockExtensions.UpdateTextBlockOnCollectionChanged;
TextBlockExtensions.CollectionToTextBlockMap.Add(
observableCollection,
new List<WeakReference<TextBlock>>() {new WeakReference<TextBlock>(textBlock)});
}
}
private static void UpdateTextBlockOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (TextBlockExtensions.CollectionToTextBlockMap.TryGetValue(
sender as INotifyCollectionChanged,
out IList<WeakReference<TextBlock>> boundTextBlocks))
{
var textBlockReferences = boundTextBlocks.ToList();
foreach (WeakReference<TextBlock> boundTextBlockReference in textBlockReferences)
{
if (boundTextBlockReference.TryGetTarget(out TextBlock textBlock))
{
UpdateCollection(textBlock, e);
}
else
{
// TextBlock already collected by the GC. Cleanup.
boundTextBlocks.Remove(boundTextBlockReference);
}
}
}
}
private static void UpdateCollection(TextBlock textBlock, NotifyCollectionChangedEventArgs eventArgs)
{
switch (eventArgs.Action)
{
case NotifyCollectionChangedAction.Add:
AddNewInlines(eventArgs.NewItems.OfType<Inline>(), textBlock);
break;
case NotifyCollectionChangedAction.Remove:
RemoveInlines(eventArgs.OldItems.OfType<Inline>(), textBlock);
break;
case NotifyCollectionChangedAction.Replace:
ReplaceInlines(eventArgs, textBlock);
break;
case NotifyCollectionChangedAction.Move:
MoveInlines(eventArgs, textBlock);
break;
case NotifyCollectionChangedAction.Reset:
textBlock.Inlines.Clear();
break;
}
}
private static void AddNewInlines(IEnumerable<Inline> newItems, TextBlock textBlock)
{
foreach (Inline newItem in newItems)
{
textBlock.Inlines.Add(newItem);
}
}
private static void RemoveInlines(IEnumerable<Inline> removedItems, TextBlock textBlock)
{
foreach (Inline removedItem in removedItems)
{
textBlock.Inlines.Remove(removedItem);
}
}
private static void ReplaceInlines(NotifyCollectionChangedEventArgs eventArgs, TextBlock textBlock)
{
int currentReplaceIndex = eventArgs.NewStartingIndex;
List<Inline> replacedItems = eventArgs.OldItems.OfType<Inline>().ToList();
List<Inline> replacementItems = eventArgs.NewItems.OfType<Inline>().ToList();
for (int changedItemsIndex = 0; changedItemsIndex < replacementItems.Count; changedItemsIndex++)
{
Inline replacedItem = textBlock.Inlines.ElementAt(currentReplaceIndex++);
Inline replacementItem = replacementItems.ElementAt(changedItemsIndex);
textBlock.Inlines.InsertAfter(replacedItem, replacementItem);
textBlock.Inlines.Remove(replacedItem);
}
}
private static void MoveInlines(NotifyCollectionChangedEventArgs eventArgs, TextBlock textBlock)
{
foreach (Inline movedItem in eventArgs.OldItems.OfType<Inline>())
{
Inline currentItemAtNewPosition = textBlock.Inlines.ElementAt(eventArgs.NewStartingIndex);
textBlock.Inlines.Remove(movedItem);
textBlock.Inlines.InsertAfter(currentItemAtNewPosition, movedItem);
}
}
}