PropertyChangedCallback执行顺序

时间:2013-03-18 16:49:08

标签: .net wpf mvvm

我有一个具有两个DependencyProperties的用户控件。每个DependencyProperty都有PropertyChangedCallback。在订单属性值设置中调用是很重要的回调。所以,如果我写

Status = MyStatus.DataIsImporting;

var data = AsynchronouslyImportData();

Data = data;

我希望在Data的属性更改回调之前调用Status的属性更改回调。但根据调试(找不到任何关于它的文档),回调调用的顺序是未定义的。有什么方法可以解决它吗?

更新。您在上面看到的状态和数据不会直接设置为用户控件实例。这些是ViewModel属性,它们通过绑定来控制用户属性。

UPDATE2 即可。我现在正在玩这个问题并且有一个非常奇怪的修复。以下是我的用户控件的使用方式:

<MyUserControl Status={Binding Status, Mode=TwoWay} Data={Binding Data}/>

我刚刚更改了绑定顺序,它有效!

<MyUserControl Data={Binding Data} Status={Binding Status, Mode=TwoWay}/>

它仍然表现为异步(看起来绑定系统中存在一种消息循环)但现在它以正确的顺序调用PropertyChangedCallback处理程序。

我正在使用Google搜索绑定顺序并不时发现类似的问题(例如this one),但仍然不清楚它为什么会发生。

更新3。我找到了一个真正的问题来源。使用我的控件的应用程序具有ContentControl和几个DataTemplates(取决于ViewModel类型)。当放置我的控件的DataTemplate不是最新的时(或当您切换到其他DataTemplate并返回时),会发生描述的行为。我还在澄清细节。

2 个答案:

答案 0 :(得分:0)

我应该在这个陈述中为这个答案做准备:

“如果您需要通过安排/排序DependencyProperty的顺序来对DependencyPropertyChangedCallbacks进行更改,那么您可能做错了。”

那就是说,这里有一些闲置的代码,有点做你正在谈论的事情:

对象:

public class SomeThing : DependencyObject, IDisposable
{
    public static readonly DependencyProperty StatusProperty = 
        DependencyProperty.Register(
            "Status", 
            typeof(string), 
            typeof(SomeThing), 
            new FrameworkPropertyMetadata(OnStatusChanged));
    public static readonly DependencyProperty DataProperty = 
        DependencyProperty.Register(
            "Data", 
            typeof(string), 
            typeof(SomeThing), 
            new FrameworkPropertyMetadata(OnDataChanged));

    // The OrderedBag is from the Wintellect.PowerCollections, 
    // as I was too lazy to write my own PriorityQueue-like implementation
    private static OrderedBag<Tuple<int, DependencyObject, DependencyPropertyChangedEventArgs>> _changeQueue = 
        new OrderedBag<Tuple<int, DependencyObject, DependencyPropertyChangedEventArgs>>((l,r) => l.Item1.CompareTo(r.Item1));

    private static object _syncRoot = new object();
    private static Task queueTenderTask;
    private static CancellationTokenSource canceller;

    static SomeThing()
    {
        canceller = new CancellationTokenSource();
        queueTenderTask = Task.Factory.StartNew(queueTender);
    }

    public string Status
    {
        get { return (string)this.GetValue(StatusProperty); }
        set { this.SetValue(StatusProperty, value); }
    }
    public string Data
    {
        get { return (string)this.GetValue(DataProperty); }
        set { this.SetValue(DataProperty, value); }
    }

    public void Dispose()
    {
        if(canceller != null)
        {
            canceller.Cancel();
            if(queueTenderTask != null)
            {
                queueTenderTask.Wait();
            }
        }
    }

    private static void OnStatusChanged(
        DependencyObject dobj, 
        DependencyPropertyChangedEventArgs args)
    {
        lock(_syncRoot)
        {
            _changeQueue.Add(Tuple.Create(0, dobj, args));
        }
    }
    private static void OnDataChanged(
        DependencyObject dobj, 
        DependencyPropertyChangedEventArgs args)
    {
        lock(_syncRoot)
        {
            _changeQueue.Add(Tuple.Create(1, dobj, args));
        }
    }
    private static void ProcessChange(
        Tuple<int, DependencyObject,DependencyPropertyChangedEventArgs> pair)
    {
        // do something useful?
        Console.WriteLine(
            "Processing change on {0} from {1} to {2}", 
            pair.Item3.Property.Name, 
            pair.Item3.OldValue, 
            pair.Item3.NewValue);
    }

    private static void queueTender()
    {
        Console.WriteLine("Starting queue tender...");
        var shouldCancel = canceller.IsCancellationRequested;
        while(!shouldCancel)
        {
            lock(_syncRoot)
            {
                if(_changeQueue.Count > 0)
                {
                    var nextUp = _changeQueue[0];
                    _changeQueue.RemoveFirst();    
                    ProcessChange(nextUp);
                }
            }
            for(int i=0;i<10;i++)
            {
                shouldCancel = canceller.IsCancellationRequested;
                if(shouldCancel) break;
                Thread.Sleep(10);
            }
        }
    }
}

测试:

void Main()
{
    var rnd = new Random();
    using(var ob = new SomeThing())
    {
        for(int i=0;i<10;i++)
        {
            if(rnd.NextDouble() > 0.5)
            {
                Console.WriteLine("Changing Status...");
                ob.Status = rnd.Next(0, 100).ToString();
            }
            else
            {
                Console.WriteLine("Changing Data...");
                ob.Data = rnd.Next(0, 100).ToString();
            }
        }
        Console.ReadLine();
    }
}

输出:

Starting queue tender...
Changing Status...
Changing Status...
Changing Status...
Changing Data...
Changing Data...
Changing Data...
Changing Data...
Changing Data...
Changing Data...
Changing Status...
Processing change on Status from  to 1
Processing change on Status from 1 to 73
Processing change on Status from 73 to 57
Processing change on Status from 57 to 33
Processing change on Data from  to 10
Processing change on Data from 10 to 67
Processing change on Data from 67 to 40
Processing change on Data from 40 to 64
Processing change on Data from 64 to 47
Processing change on Data from 47 to 81

答案 1 :(得分:-1)

根据您的评论,这些属性不必是DependencyProperties。它们是TwoWay绑定的来源,但这并不要求它们是DependencyProperties。您可以对任何标准属性使用TwoWay绑定,并实现INotifyPropertyChanged以便在源值更改时通知目标。

Binding目标必须是DependencyProperty,而不是来源,即使对于TwoWay绑定(TextBoxes绑定TwoWay默认情况下也是如此可以将它们绑定到标准属性。)

如果您手动提出属性更改通知,则这些操作将在同一个线程上按顺序发生,并且您将保证按顺序发生通知。使用DependencyProperties,您可以让框架管理值和通知,并且无法保证订单。

ViewModel中很少需要

DependencyProperties。大多数情况下,您只需要ViewModel来实现INotifyPropertyChanged并提出属性更改通知。