使用BackgroundWorker交换绑定属性的对象引用

发布于 2024-10-08 15:37:32 字数 4897 浏览 1 评论 0原文

一点背景知识

我正在构建一个使用插件架构来访问特定设备的应用程序。这些设备有一些字段,我将其命名为“属性”,这些字段可以读取、写入或两者兼而有之。这是通过使用在其自己的线程中运行的 WPF 前端和后端中的类似 MVVM 的结构来完成的,该结构包装特定的插件以以通用方式公开其属性。

有关线程结构的一些细节

我有两个“主机”对象,一个启动插件结构,最终公开两个视图:可用插件的视图和(如果选择了插件)单个插件已知的所有属性的视图。另一个最终启动 STA 线程并运行 WPF 应用程序中的主窗口。第二个主机使用BackgroundWorkers来执行选择、初始化、更新和提交任务。 DoWork 事件/委托(我相信它们被称为委托)是在 ViewController 类中定义的,该类位于 MVVM 结构中,并提供用于更新等的功能接口,并实现 INotifyPropertyChanged 接口。

更多细节

创建主窗口后,它的上下文被设置为视图控制器对象。然后 GUI 将两个列表框绑定到两个视图。

问题

当我从 UI 线程中调用 selectPlugin 方法时,它会冻结,直到连接操作完成并且每个属性都加载到 ViewModel 包含的列表中。但是它确实有效,之后 ListBox itemssource 绑定会更新并显示所有属性。

不过,我不希望 UI 在每次操作时都冻结,因此我实现了后台工作人员。一切正常,对象得到更新,绑定源被新的 View 实例替换。但绑定本身不会更新。

我尝试在 UI 线程中使用 Dispatcher.Invoke 或 DoWorkComplete 事件分配不同的解决方案。我发现使用以下属性设置器,PropertyChanged 事件本身保持为 null:

    public IAttributesViewModel AttributesView
    {
        get 
        {
                StaticLogger.WriteDebugLog(log,
                                           String.Format("Executing {0}",
                                                         System.Reflection.MethodBase.GetCurrentMethod()));
                return _currentAttributes;
        }
        set 
        {
            _currentAttributes = value;
            OnPropertyChanged("AttributesView");
        }
    }

INotifyPropertyChanged 的​​实现如下所示:

    #region INotifyPropertyChange implementation
    /// <summary>
    /// Occurs when a property value changes.
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;
    /// <summary>
    /// Called when [property changed].
    /// </summary>
    /// <param name="name">The name.</param>
    protected void OnPropertyChanged(string name)
    {
        StaticLogger.WriteDebugLog(log, String.Format("Executing {0}", System.Reflection.MethodBase.GetCurrentMethod()));
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(name));
        }
    }
    #endregion

上面声明的 PropertyChanged 事件始终为 null。这可能或应该与 XAML 中的实际绑定有关,但是我确实在 UI 线程中设置了主窗口的数据上下文,如下所示:

    private static void Host(object viewControler)
    {
        var controler = viewControler as IViewController;
        _instance._main = new MainWindow(controler) { DataContext = controler };
        var gui = new App();
        if (_instance._main == null) throw new ArgumentNullException("viewControler");
        gui.Run(_instance._main);
    }

我在 UI 主机对象之外使用 Singleton 实例,以防产生影响。

在 XAML 中,我在包含 AttributesListbox 和一些样式代码的 UserControll 上使用自定义依赖属性。实际的 ListBox 因其自己的 ItemSource 属性而绑定到此属性。不要认为这应该与直接使用列表框不同,但以防万一这会导致 PropertyChanged 事件为空的问题,并通过以下方式实现:

C# /// /// AttributesListBox.xaml 的交互逻辑 /// 公共部分类 AttributesListBox : UserControl { 私有静态只读 UIPropertyMetadata _sourceMetadata = new UIPropertyMetadata(String.Empty, SourceChangedCallback);

    public AttributesListBox()
    {
        InitializeComponent();
    }

    /// <summary>
    /// The dependency property that gets or sets the source of the ListBox to render.
    /// </summary>
    public static DependencyProperty sourceProperty = DependencyProperty.Register(
        "Source", typeof(IEnumerable), typeof(AttributesListBox),_sourceMetadata);
    /// <summary>
    /// Gets or sets the nsource of the image to render.
    /// </summary>
    public IEnumerable Source
    {
        get { return (IEnumerable)GetValue(sourceProperty); }
        set { SetValue(sourceProperty, value); }
    }

    private static void SourceChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var box = (AttributesListBox) d;
        box.List.ItemsSource = e.NewValue as IEnumerable;
    }
}

XAML

<ListBox Name="List" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" ItemsSource="{Binding Source}" BorderBrush="{x:Null}" />

第一个问题

我将 ViewController 对象与 UI 结合使用的方式有什么问题?那么为什么 PropertyChanged 事件总是 null 呢?这应该意味着我在某个地方搞乱了绑定过程,不是吗?

第二个问题

如何获取 OnPropertyChanged 事件来通知 UI 线程,具体化 UserControll(AttributesListbox) 中的 Binding?

活动遵循什么路线?

例如: 我使用 DoWork 委托,此方法直接从实例内部更改属性,而不是在 UI 线程中。这是否会导致事件永远不会到达 UI 线程,因为它是在其他工作线程中引发的?

我无法理解事件冒泡/隧道,事件是否仅限于创建实例的线程或调用特定方法的线程(使用调度程序或其他方法)?

假设

我怀疑我的案例有两个问题: 1. PropertyChanged 处理程序保持为 null,因为该对象未绑定或未在正确的线程中创建。 2. 该事件(如果实际触发)永远不会到达正确的线程,因为它卡在BackgroundWorker 线程或后端“主机”线程中。

这是我在这里问的第一个问题,所以如果您遗漏了一些拼图,请通知我。

感谢您花时间阅读我的小问题情况,我希望我们能够解决这个问题一个解决方案。

A little background

I'm building an application which uses a plug-in architecture to access specific devices. These devices have fields, which I named Attributes, that can be read, written or both. This is done using a WPF front-end running in it's own thread and a MVVM like structure in the back end which wraps specific plug-in to expose it's attributes in a generic way.

Some details about the threading structure

I have two "host" objects, one initiates the plug-in structures eventually exposes two views: a view of available plug ins and (if a plug-in was selected) a view of all attributes known by a single plug-in. The other eventually starts a STA-thread and runs the a main window in a WPF application. The second host uses BackgroundWorkers to do select, initialization, update and submit tasks. The DoWork events/delegates(I believe they are called delegates) are defined in a ViewController class which is in the MVVM structure and provides a functional interface for updating etc. and implements the INotifyPropertyChanged interface.

Some more details

Upon creation of the mainwindow it's context is set to the view controller object. The GUI then binds two list boxes to both views.

The problem

When I call the selectPlugin method from within the UI thread it freezes until the connect operation is complete and every single attribute is loaded into the list contained by the ViewModel. However it does work and afterwards the ListBox itemssource binding is updated and all attributes are shown.

However I don't want the UI to freeze up on every operation, so I implemented backgroundworkers. It all works fine, the objects get updated and the binding source is replaced by a new View instance. But the binding itself doesn't update.

I tried allot off different solutions using the Dispatcher.Invoke or the DoWorkComplete event in the UI thread. I found that the PropertyChanged event itself stays null using the following property setter:

    public IAttributesViewModel AttributesView
    {
        get 
        {
                StaticLogger.WriteDebugLog(log,
                                           String.Format("Executing {0}",
                                                         System.Reflection.MethodBase.GetCurrentMethod()));
                return _currentAttributes;
        }
        set 
        {
            _currentAttributes = value;
            OnPropertyChanged("AttributesView");
        }
    }

The implementation of INotifyPropertyChanged looks like this:

    #region INotifyPropertyChange implementation
    /// <summary>
    /// Occurs when a property value changes.
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;
    /// <summary>
    /// Called when [property changed].
    /// </summary>
    /// <param name="name">The name.</param>
    protected void OnPropertyChanged(string name)
    {
        StaticLogger.WriteDebugLog(log, String.Format("Executing {0}", System.Reflection.MethodBase.GetCurrentMethod()));
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(name));
        }
    }
    #endregion

The PropertyChanged event as declared above is always null. This could or should have something to do with the actual binding in XAML, however i did set de datacontext of the main window in the UI thread like this:

    private static void Host(object viewControler)
    {
        var controler = viewControler as IViewController;
        _instance._main = new MainWindow(controler) { DataContext = controler };
        var gui = new App();
        if (_instance._main == null) throw new ArgumentNullException("viewControler");
        gui.Run(_instance._main);
    }

I use a Singleton instance off the UI host object, in case that makes a difference.

In XAML I use a custom dependency property on a UserControll containing the AttributesListbox and some styling code. The actual ListBox binds to this property for it's own ItemSource property. Don't think this should be different from using the list box directly, but just in case this causes the issue with the PropertyChanged event being null, and is implemented in the following way:

C#
///
/// Interaction logic for AttributesListBox.xaml
///
public partial class AttributesListBox : UserControl
{
private static readonly UIPropertyMetadata _sourceMetadata = new UIPropertyMetadata(String.Empty,
SourceChangedCallback);

    public AttributesListBox()
    {
        InitializeComponent();
    }

    /// <summary>
    /// The dependency property that gets or sets the source of the ListBox to render.
    /// </summary>
    public static DependencyProperty sourceProperty = DependencyProperty.Register(
        "Source", typeof(IEnumerable), typeof(AttributesListBox),_sourceMetadata);
    /// <summary>
    /// Gets or sets the nsource of the image to render.
    /// </summary>
    public IEnumerable Source
    {
        get { return (IEnumerable)GetValue(sourceProperty); }
        set { SetValue(sourceProperty, value); }
    }

    private static void SourceChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var box = (AttributesListBox) d;
        box.List.ItemsSource = e.NewValue as IEnumerable;
    }
}

XAML

<ListBox Name="List" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" ItemsSource="{Binding Source}" BorderBrush="{x:Null}" />

The first question(s)

What is wrong with the way I use the ViewController object in conjunction with the UI? And thus why is the PropertyChanged event always null? This should mean I messed up the binding process somewhere doesn't it?

The second question(s)

How do I get the OnPropertyChanged event to notify the UI thread, specificity the Binding in the UserControll(AttributesListbox)?

What route does the event follow?

for instance:
I use a DoWork delegate and this method changes the property directly from within the instance, which is not in the UI thread. Does this cause the event never to reach the UI thread because is it raised in the other worker thread?

I can't get my head around the event bubbling/tunnelling, is a event restricted to the thread which created the instance or the thread which called the specific method(using a dispatcher or whatever)?

Hypothesis

I suspect there are two issues in my case:
1. The PropertyChanged handler stays null because the object is either not bound or because it is not created in the correct thread.
2. The event, if it where actually fired, never reaches the correct thread because it gets stuck in either the BackgroundWorker thread or the back end "host" thread.

This is the first question I have asked here, so if your missing some pieces off the puzzle please do inform me.

Thanks for taking the time to read about my little problem situation, I hope we can come to a solution.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

过度放纵 2024-10-15 15:37:32

我认为您可能搞乱了列表框的绑定,即(为简单起见而缩短):

<ListBox ItemsSource="{Binding Source}" />

此绑定意味着“在我当前的 DataContext 中查找名为 Source 的属性” ,我怀疑这不是你想要的。如果你想绑定到 AttributesListBox 的属性 Source (我想它承载着上面的列表框),你应该这样做:

<ListBox ItemsSource="{Binding Source, RelativeSource={RelativeSource AncestorType={x:Type AttributesListBox}}}" />

这意味着 - “寻找一个树上第一个类型为 AttributesListBox 的对象中名为 Source 的属性。当然,您仍然必须将该 Source 属性绑定到控制器的正确内容,但我想您已经这样做了。

处理属性更改事件的方法是它必须从 UI(Dispatcher)线程引发,如果从后台线程引发它,wpf 不会自动将其封送到 UI 线程。因此,请确保当后台工作完成后,您设置了需要在 UI 线程上更新的属性。

I think you may have messed up the binding of your listbox, namely (shortened for simplicity):

<ListBox ItemsSource="{Binding Source}" />

this binding means "look for a property called Source in the my current DataContext", which I suspect is not what you intend. If you want to bind to the property Source of the AttributesListBox (which I suppose hosts the above listbox), you should do it this way:

<ListBox ItemsSource="{Binding Source, RelativeSource={RelativeSource AncestorType={x:Type AttributesListBox}}}" />

which means - "look for a property called Source in the first object up the tree that is of type AttributesListBox". Of cource you still have to bind that Source property to the correct thing of your controller, but I suppose you have done that.

The deal with the property changed event is that it must be raised from the UI (Dispatcher) thread, if you raise it from a background thread wpf will not automatically marshal it to the UI thread. So make sure that when the background work has completed, you set the propoperties which need to be updated on the UI thread.

云雾 2024-10-15 15:37:32

提出问题的好处在于,它迫使你真正思考问题的原因,因此稍微休息一下,你可能最终会在两天后找到完全简单的解决方案。

问题是 ParameterizedThreadStart 请求一个 void 方法(object arg),因此只要您传递一个对象,编译器就不会进行类型检查。我传递了错误的对象,因为我编辑了该方法。使用“as”运算符的安全类型转换似乎很痛苦,它似乎吞下了 CastingException,因此没有通知您这个小错误。

简单的解决方案是:

private static void Host(object viewControler)
{
    var controler = viewControler as IViewController;
    _instance._main = new MainWindow(controler) { DataContext = controler };
    var gui = new App();
    if (_instance._main == null) throw new ArgumentNullException("viewControler");
    gui.Run(controler);
}

我没想到我的BackgroundWorker 能够100%“开箱即用”,但它似乎确实如此。我的代码是否是“最佳实践”是另一个单独的问题。

让我朝这个方向思考的关键是:
简短示例:数据绑定和对象初始化的坏习惯和好习惯< /a>

在 Visaul Studio 中使用 DataBindings 时,还可以查看这篇小文章:
如何显示 WPF 跟踪

The great thing about asking a question is that is forces you to really think on de cause off your problem, thus taking a little break you might finally, after two days, find the utterly simple solution.

The problem was a ParameterizedThreadStart which request a void method(object arg) so the compiler does no type checking what so ever as long as you pass a object. An I passed the wrong object because I edited the method. The safe typecasting using the "as" operator seems to be a pain to, it seems to swallow an CastingException thus not informing you about the small mistake.

So simple solution is:

private static void Host(object viewControler)
{
    var controler = viewControler as IViewController;
    _instance._main = new MainWindow(controler) { DataContext = controler };
    var gui = new App();
    if (_instance._main == null) throw new ArgumentNullException("viewControler");
    gui.Run(controler);
}

I didn't expect my BackgroundWorker to work a 100% "out of the box" but it seems to do so. Whether my code is "best practice" is another separate question.

The key to making me think in that direction was:
Short example: bad and good practice for databinding and object initialization

When working with DataBindings in visaul studio also check out this small article:
How to show WPF trace

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文