wpf DataContext Change 不会更新“附加行为”中的绑定

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/13395201/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-13 06:13:49  来源:igfitidea点击:

DataContext Change does not update the Binding in an "Attached Behavior"

wpfinfragisticsattachedbehaviors

提问by rfcdejong

A while ago i wrote an "Attached Behavior" for two way syncronisation between a XamDataGrid and a "Business Object" as ObservableCollection. The XamDataGrid is the source and the ObservableCollection as DataSource is the Target. I did not use a ListCollectionView since for specific reasons.

不久前,我为 XamDataGrid 和作为 ObservableCollection 的“业务对象”之间的双向同步编写了“附加行为”。XamDataGrid 是源,ObservableCollection 作为 DataSource 是目标。由于特定原因,我没有使用 ListCollectionView。

The problem

问题

When the DataContext of the DataGrid is changed to another Vehicle the currently loaded DataGrid does not update the DependencyProperty of the behavior.

当 DataGrid 的 DataContext 更改为另一个 Vehicle 时,当前加载的 DataGrid 不会更新行为的 DependencyProperty。

I cannot figure out why.

我不明白为什么。

The only solution i can think of is to hook the DataContextChanged of the DataGrid and do a new BindingOperation with a Path that is then set relative to the DataContext to figure out the SelectedItems property. But in that case the DependencyProperty of the behavior should be set to a Path to the SelectedItems property and not a binding.

我能想到的唯一解决方案是挂钩 DataGrid 的 DataContextChanged 并使用 Path 执行新的 BindingOperation,然后相对于 DataContext 设置以找出 SelectedItems 属性。但在这种情况下,行为的 DependencyProperty 应设置为 SelectedItems 属性的路径,而不是绑定。

Having the following classes

有以下课程

A example model

示例模型

public class Vehicle 
{ 
    public PassengerList Passengers { get; set; } 
}

public class PassengerList : ObservableCollection<Passenger> 
{
    public PassengerList()
    {
         SelectedPassengers = new ObservableCollection<Passenger>();
    }

    public ObservableCollection<Passenger> SelectedPassengers { get; private set; }
}

public class Passenger
{
    public string Name { get; set; }
}

The xaml

xaml

<igDG:XamDataGrid DataSource="{Binding Passengers}">
<i:Interaction.Behaviors>
   <b:XamDataGridSelectedItemsBehavior SelectedItems="{Binding Path=Passengers.SelectedPssengers}" />
</i:Interaction.Behaviors>
</igDG:XamDataGrid>

PS: I have also tried a Element binding to the DataGrid as element but that doesn't fix it. The DependencyProperty is set only once. For example the Models

PS:我也尝试过将元素绑定到 DataGrid 作为元素,但这并没有解决。DependencyProperty 只设置一次。例如模型

The two way behavior

双向行为

When an selected item changes in the model the grid selected item has to be updated as well. It also does not need to be detached as well, using weak events.

当模型中的选定项更改时,网格选定项也必须更新。它也不需要分离,使用弱事件。

public class XamDataGridSelectedItemsBehavior : Behavior<XamDataGrid>, IWeakEventListener
{
    #region Properties

    private XamDataGrid Grid
    {
        get { return AssociatedObject as XamDataGrid; }
    }

    public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(
        "SelectedItems",
        typeof(INotifyCollectionChanged),
        typeof(XamDataGridSelectedItemsBehavior),
        new UIPropertyMetadata(new PropertyChangedCallback(OnSelectedItemsChanged)));

    private static void OnSelectedItemsChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) 
    {
         if (obj != null) 
         {
            (obj as XamDataGridSelectedItemsBehavior).SelectedItems = (e.NewValue as INotifyCollectionChanged);
        }
    }

    public INotifyCollectionChanged SelectedItems
    {
        get { return (INotifyCollectionChanged)GetValue(SelectedItemsProperty); }
        set
        {
            // remove old listener
            if (SelectedItems != null)
                CollectionChangedEventManager.RemoveListener(SelectedItems, this);

            SetValue(SelectedItemsProperty, value);

            // add new listener
            if (SelectedItems != null)
                CollectionChangedEventManager.AddListener(SelectedItems, this);
        }
    }

    #endregion

    #region Init

    /// <summary>
    /// Hook up event listeners to the associated object.
    /// </summary>
    protected override void OnAttached()
    {
        base.OnAttached();

        SelectedItemsChangedEventManager.AddListener(Grid, this);
        XamDataGridRecordActivatedEventManager.AddListener(Grid, this);
        XamDataGridLoadedEventManager.AddListener(Grid, this);
    }

    void Grid_RecordActivated(object sender, RecordActivatedEventArgs e)
    {
        if (_transferingToTarget)
            return;

        // if the CellClickAction is EnterEditModeIfAllowed, the grid does not always select the actual record
        // In our case we want it to always select the record
        if (e.Record.DataPresenter.FieldSettings.CellClickAction == CellClickAction.EnterEditModeIfAllowed)
        {
            TransferSourceToTarget();
        }
    }

    void Grid_Loaded(object sender, RoutedEventArgs e)
    {
        TransferTargetToSource(true);
    }

    #endregion

    #region Target to Source

    /// <summary>
    /// When selected items in the target as model has changed, then transfer selected item to grid as the source.
    /// Not when transfering from grid to selected items.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void SelectedItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (_transferingToTarget)
            return;

        TransferTargetToSource(false);
    }

    private bool _transferingToSource = false;
    /// <summary>
    /// Transfer selected item in the target as model to the grid as source.
    /// </summary>
    private void TransferTargetToSource(bool notifyTargetListeners)
    {
        if (SelectedItems == null)
            return;

        List<Record> newSelection = new List<Record>();
        foreach (var item in (SelectedItems as IList))
        {
            var record = Grid.Records.FirstOrDefault(r => (r is DataRecord) && ((r as DataRecord).DataItem == item));
            if (record != null)
            {
                newSelection.Add(record);
            }
        }

        _transferingToSource = true;
        try
        {
            Grid.SelectedItems.Records.Clear();
            Grid.SelectedItems.Records.AddRange(newSelection.ToArray());
            if ((newSelection.Count > 0) && !newSelection.Contains(Grid.ActiveRecord))
            {
                Grid.ActiveRecord = newSelection.FirstOrDefault();
                Grid.ActiveRecord.IsSelected = true;
            }

            if (notifyTargetListeners)
            {
                // Hack to notify the target listeners
                (SelectedItems as IList).Clear();
                foreach (var record in newSelection)
                {
                    (SelectedItems as IList).Add((record as DataRecord).DataItem);
                }
            }
        }
        finally
        {
            _transferingToSource = false;
        }
    }

    #endregion

    #region Source to Target

    /// <summary>
    /// When selected items in the source as grid has changed, then transfer selected item to model as the target.
    /// Not when transfering from selected items to grid.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void Grid_SelectedItemsChanged(object sender, SelectedItemsChangedEventArgs e)
    {
        if (_transferingToSource)
            return;

        TransferSourceToTarget();
    }

    private bool _transferingToTarget = false;
    /// <summary>
    /// Transfer the selected item in the grid as source to the selected item in the target as model.
    /// </summary>
    private void TransferSourceToTarget()
    {
        var target = this.SelectedItems as IList;
        if (target == null)
            return;

        _transferingToTarget = true;
        try
        {
            // clear the target first
            target.Clear();

            // When no item is selected there might still be an active record
            if (Grid.SelectedItems.Count() == 0)
            {
                if (Grid.ActiveDataItem != null)
                    target.Add(Grid.ActiveDataItem);
                else if (Grid.ActiveRecord != null && Grid.ActiveRecord.IsDataRecord)
                    target.Add((Grid.ActiveRecord as DataRecord).DataItem);
                else if (Grid.ActiveCell != null && Grid.ActiveCell.Record != null && Grid.ActiveCell.Record.IsDataRecord)
                    target.Add((Grid.ActiveCell.Record as DataRecord).DataItem);
            }
            else
            {
                // foreach record in the source add it to the target
                foreach (var r in Grid.SelectedItems.Records)
                {
                    if (r.IsDataRecord)
                    {
                        target.Add((r as DataRecord).DataItem);
                    }
                }
            }
        }
        finally
        {
            _transferingToTarget = false;
        }
    }

    #endregion

    /// <summary>
    /// Receive an event and delegate it to the correct eventhandler.
    /// </summary>
    /// <param name="managerType"></param>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    /// <returns></returns>
    bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
    {
        if (managerType == typeof(CollectionChangedEventManager))
        {
            SelectedItems_CollectionChanged(sender, e as NotifyCollectionChangedEventArgs);
            return true;
        }
        else if (managerType == typeof(SelectedItemsChangedEventManager))
        {
            Grid_SelectedItemsChanged(sender, e as SelectedItemsChangedEventArgs);
            return true;
        }
        else if (managerType == typeof(XamDataGridRecordActivatedEventManager))
        {
            Grid_RecordActivated(sender, e as RecordActivatedEventArgs);
            return true;
        }
        else if (managerType == typeof(XamDataGridLoadedEventManager))
        {
            Grid_Loaded(sender, e as RoutedEventArgs);
            return true;
        }
        return false;
    }
}

#region EventManagers

public class CollectionChangedEventManager : WeakEventManagerBase<CollectionChangedEventManager, INotifyCollectionChanged>
{
    protected override void StartListeningTo(INotifyCollectionChanged source)
    {
        source.CollectionChanged += DeliverEvent;
    }

    protected override void StopListeningTo(INotifyCollectionChanged source)
    {
        source.CollectionChanged -= DeliverEvent;
    }
}

public class XamDataGridRecordActivatedEventManager : WeakEventManagerBase<XamDataGridRecordActivatedEventManager, XamDataGrid>
{
    protected override void StartListeningTo(XamDataGrid source)
    {
        source.RecordActivated += DeliverEvent;
    }

    protected override void StopListeningTo(XamDataGrid source)
    {
        source.RecordActivated -= DeliverEvent;
    }
}

public class XamDataGridLoadedEventManager : WeakEventManagerBase<XamDataGridLoadedEventManager, XamDataGrid>
{
    protected override void StartListeningTo(XamDataGrid source)
    {
        source.Loaded += DeliverEvent;
    }

    protected override void StopListeningTo(XamDataGrid source)
    {
        source.Loaded -= DeliverEvent;
    }
}

public class SelectedItemsChangedEventManager : WeakEventManagerBase<SelectedItemsChangedEventManager, XamDataGrid>
{
    protected override void StartListeningTo(XamDataGrid source)
    {
        source.SelectedItemsChanged += DeliverEvent;
    }

    protected override void StopListeningTo(XamDataGrid source)
    {
        source.SelectedItemsChanged -= DeliverEvent;
    }
}

#endregion

#region EventManager base class

// TODO: 10-10-2011 (rdj): Deze class misschien opnemen in het frontend framework? In ieder geval zolang we nog geen .NET 4.5 gebruiken
// http://10rem.net/blog/2012/02/01/event-handler-memory-leaks-unwiring-events-and-the-weakeventmanager-in-wpf-45

/// <summary>
/// Weak event manager base class to provide easy implementation of weak event managers.
/// </summary>
/// <typeparam name="TManager">Type of the manager.</typeparam>
/// <typeparam name="TEventSource">Type of the event source.</typeparam>
public abstract class WeakEventManagerBase<TManager, TEventSource> : WeakEventManager
    where TManager : WeakEventManagerBase<TManager, TEventSource>, new()
    where TEventSource : class
{
    /// <summary>
    /// Adds a listener
    /// </summary>
    /// <param name="source">The source of the event, should be null if listening to static events</param>
    /// <param name="listener">The listener of the event. This is the class that will recieve the ReceiveWeakEvent method call</param>
    public static void AddListener(object source, IWeakEventListener listener)
    {
        CurrentManager.ProtectedAddListener(source, listener);
    }

    /// <summary>
    /// Removes a listener
    /// </summary>
    /// <param name="source">The source of the event, should be null if listening to static events</param>
    /// <param name="listener">The listener of the event. This is the class that will recieve the ReceiveWeakEvent method call</param>
    public static void RemoveListener(object source, IWeakEventListener listener)
    {
        CurrentManager.ProtectedRemoveListener(source, listener);
    }

    /// <inheritdoc/>
    protected sealed override void StartListening(object source)
    {
        StartListeningTo((TEventSource)source);
    }

    /// <inheritdoc/>
    protected sealed override void StopListening(object source)
    {
        StopListeningTo((TEventSource)source);
    }

    /// <summary>
    /// Attaches the event handler.
    /// </summary>
    protected abstract void StartListeningTo(TEventSource source);

    /// <summary>
    /// Detaches the event handler.
    /// </summary>
    protected abstract void StopListeningTo(TEventSource source);

    /// <summary>
    /// Gets the current manager
    /// </summary>
    protected static TManager CurrentManager
    {
        get
        {
            var mType = typeof(TManager);
            var mgr = (TManager)GetCurrentManager(mType);
            if (mgr == null)
            {
                mgr = new TManager();
                SetCurrentManager(mType, mgr);
            }
            return mgr;
        }
    }
}

#endregion

回答by rfcdejong

I have it working by adding two new Dependency Properties.

我通过添加两个新的依赖属性来让它工作。

DataContextProperty

数据上下文属性

    public static readonly DependencyProperty DataContextProperty = DependencyProperty.Register(
        "DataContext",
        typeof(object),
        typeof(XamDataGridSelectedItemsBehavior),
        new PropertyMetadata(DataContextChanged));

    private static void DataContextChanged(object obj, DependencyPropertyChangedEventArgs e)
    {
        var behavior = obj as XamDataGridSelectedItemsBehavior;
        var binding = new Binding(behavior.Path) { Source = e.NewValue };
        BindingOperations.SetBinding(behavior, XamDataGridSelectedItemsBehavior.SelectedItemsProperty, binding);
    }

PathPropertyto use to create a new binding on whenever the DataContext has changed

PathProperty用于在 DataContext 发生更改时创建新绑定

    public static readonly DependencyProperty PathProperty = DependencyProperty.Register(
        "Path",
        typeof(string),
        typeof(XamDataGridSelectedItemsBehavior),
        new UIPropertyMetadata(string.Empty, new PropertyChangedCallback(OnPathChanged)));

    private static void OnPathChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        var behavior = obj as XamDataGridSelectedItemsBehavior;
        behavior.Path = e.NewValue as string;
    }

    public string Path { get; set; }

The DataContext property is set in the OnAttached, so that the DataContextChanged event is being hooked into

在 OnAttached 中设置了 DataContext 属性,以便将 DataContextChanged 事件挂接到

    protected override void OnAttached()
    {
        base.OnAttached();

        SelectedItemsChangedEventManager.AddListener(Grid, this);
        XamDataGridRecordActivatedEventManager.AddListener(Grid, this);
        XamDataGridLoadedEventManager.AddListener(Grid, this);

        BindingOperations.SetBinding(this, XamDataGridSelectedItemsBehavior.DataContextProperty, new Binding());
    }

The SelectedItems dependency property is now private and slightly modified

SelectedItems 依赖项属性现在是私有的并且稍作修改

    public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(
        "SelectedItems",
        typeof(INotifyCollectionChanged),
        typeof(XamDataGridSelectedItemsBehavior2),
        new UIPropertyMetadata(new PropertyChangedCallback(OnSelectedItemsChanged)));

    private static void OnSelectedItemsChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        var behavior = obj as XamDataGridSelectedItemsBehavior;

        if (behavior.SelectedItems != null)
            CollectionChangedEventManager.RemoveListener(behavior.SelectedItems, behavior);

        if (e.NewValue is INotifyCollectionChanged)
        {
            behavior.SelectedItems = e.NewValue as INotifyCollectionChanged;
            CollectionChangedEventManager.AddListener(behavior.SelectedItems, behavior);
        }
    }

    private INotifyCollectionChanged SelectedItems
    {
        get  { return (INotifyCollectionChanged)GetValue(SelectedItemsProperty); }
        set { SetValue(SelectedItemsProperty, value); }
    }

using the behavior in xaml

使用 xaml 中的行为

<igDG:XamDataGrid DataSource="{Binding Passengers}">
<i:Interaction.Behaviors>
   <b:XamDataGridSelectedItemsBehavior Path="Passengers.SelectedPassengers" />
</i:Interaction.Behaviors>
</igDG:XamDataGrid>