1

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

Status = MyStatus.DataIsImporting;

var data = AsynchronouslyImportData();

Data = data;

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

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

更新2。我现在正在处理这个问题并且有一个非常奇怪的修复。以下是我之前使用用户控件的方式:

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

我刚刚更改了绑定顺序,它起作用了!

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

它仍然以异步方式运行(看起来绑定系统内部存在某种消息循环),但现在它以正确的顺序调用 PropertyChangedCallback 处理程序。

我在谷歌上搜索绑定顺序,不时发现类似的问题(例如,这个),但仍不清楚为什么会发生。

更新 3.我找到了问题的真正根源。使用我的控件的应用程序具有带有多个 DataTemplates 的 ContentControl(取决于 ViewModel 类型)。当我的控件所在的 DataTemplate 不是当前的(或者当您切换到其他 DataTemplate 并返回时),就会发生所描述的行为。我仍在澄清细节。

4

2 回答 2

0

我可能应该用这个陈述来作为这个答案的开头:

“如果您需要DependencyProperty通过排列/排序 的顺序来对 a 所做的更改进行排序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();
    }
}

输出:

开始排队招标...
更改状态...
更改状态...
更改状态...
更改数据...
更改数据...
更改数据...
更改数据...
更改数据...
更改数据...
更改状态...
处理状态从到 1 的更改
处理状态从 1 到 73 的更改
处理状态从 73 到 57 的更改
处理状态从 57 到 33 的更改
处理从数据到 10 的更改
处理数据从 10 到 67 的变化
处理数据从 67 到 40 的变化
处理数据从 40 到 64 的变化
处理数据从 64 到 47 的变化
处理数据从 47 到 81 的变化
于 2013-03-20T18:03:24.660 回答
-1

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

BindingTarget 必须是 a ,DependencyProperty但不是 Source ,即使对于TwoWayBinding (默认TextBoxes绑定TwoWay,您可以将它们绑定到标准属性)。

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

DependencyProperties在 ViewModel 中很少需要。大多数时候,您只需要您的 ViewModel 来实现INotifyPropertyChanged和引发属性更改通知。

于 2013-03-20T22:07:30.767 回答