绑定仅工作一次

时间:2020-04-25 14:36:52

标签: c# wpf xaml binding observablecollection

在构造函数中,当我将两个项目添加到绑定到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
    }

1 个答案:

答案 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);
    }
  }
}