schulmaster
schulmaster

Reputation: 443

C# WPF MainWindow Reference behaves differently to identical alternate reference

I have created a minimal WPF example that showcases a confusing issue I discovered in a much more complicated capacity. Essentially, sourcing a reference to the application's Main Window with Application.Current.MainWindow from an external thread reliably throws an InvalidOperationException for cross thread object access. Simply reading the reference. However! If the given MainWindow has a reference to itself stored in a field, that reference can be read with no issue. It can even be dereferenced, exemplified by accessing one of its properties. Other window fields can also be read and acted upon from the other thread. Why is obtaining the same reference(confirmed with reference equals) a cross thread offense or is not a cross thread offense depending on the way it is read? Application.Current is thread safe, so it isnt to blame. The MainWindow property has no documentation elucidating an affinity check on its getter. Why does this happen?

XAML FOR EXPERIMENT:

<Window x:Class="WPF_TEST.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:WPF_TEST"
    mc:Ignorable="d"
    Title="MainWindow" Height="350" Width="525" Background="Linen">
<Grid>

  <Grid.RowDefinitions>
      <RowDefinition/>
      <RowDefinition/>
        <RowDefinition/>
    </Grid.RowDefinitions>  


  <Grid.ColumnDefinitions>
      <ColumnDefinition/>
      <ColumnDefinition/>
      <ColumnDefinition/>
  </Grid.ColumnDefinitions>


    <Button Grid.Row="1" Grid.Column="1" Content="Test Handlers" Click="IReliablyExcept">

    </Button>




</Grid>
</Window>

CODE_BEHIND FOR EXPERIMENT

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WPF_TEST
{
 /// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    private readonly MainWindow mainWindowField;
    private bool someFlag = true;

    public MainWindow()
    {
        mainWindowField = this;
        InitializeComponent();
    }

    private async void IReliablyExcept(object sender, RoutedEventArgs e)
    {
        await Task.Run(() =>
        {
            var winRef = Application.Current.MainWindow;
        });
    }

    private async void ButIDont(object sender, RoutedEventArgs e)
    {
        await Task.Run(() =>
        {
            var winRef = mainWindowField;
        });
    }

    private async void IDontEither(object sender, RoutedEventArgs e)
    {
        await Task.Run(() =>
        {
            if (someFlag)
            {
                mainWindowField.Dispatcher.Invoke(() => { someFlag = false; });

            }

        });
    }
}
}

The Click Event handler can be rewired to try all three variations. Thanks for any insight.

Upvotes: 0

Views: 613

Answers (1)

Jf Beaulac
Jf Beaulac

Reputation: 5246

It simply is that way because the Application.Current.MainWindow property checks if the calling thread has access.

This is the code of the property from a decompiler, the this.VerifyAccess(); will reliably throw the exception.

public Window MainWindow
{
  get
  {
    this.VerifyAccess();
    return this._mainWindow;
  }
  set
  {
    this.VerifyAccess();
    if (this._mainWindow is RootBrowserWindow || this.BrowserCallbackServices != null && this._mainWindow == null && !(value is RootBrowserWindow))
      throw new InvalidOperationException(SR.Get("CannotChangeMainWindowInBrowser"));
    if (value == this._mainWindow)
      return;
    this._mainWindow = value;
  }
}

Upvotes: 1

Related Questions