简体   繁体   中英

Binding works only once

In the constructor, when I add two items to an ObservableCollection bound to an attached property of a TextBlock, the attached property is updated. But when I add items to the same ObservableCollection later in another method, the attached property is not updated.

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 (by 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
    }

Right now you only set the TextBlock inlines once.
You forgot to subscribe to the INotifyCollectionChanged.CollectionChanged event and to handle the changed items.

The following refactored version of TextBlockExtensions listens to INotifyCollectionChanged.CollectionChanged in case the binding source implements INotifyCollectionChanged and handles the changes accordingly:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM