jeudi 17 avril 2014

WPF - c# - transférer un contrôle d'un thread à un autre one - Stack Overflow


The more a window is filled with content, more it will delay to load completely, and the user will be able to use any of the controls only when it have already finished.


So I imagined the following:


Making a window empty of controls and create them on another thread, like BackgroundWorker. Then, the controls would be added to the main grid gradually, so the user could use the first controls without have to wait the rest to be created.


To try to bring it to reality, I've already tried to use some techniques. The last one was trying to use Invoke, which, actually, I don't know neither how that works nor how to use.


public MainWindow()
{
InitializeComponent();

Thread t = new Thread(new ThreadStart(CreateControls));
t.SetApartmentState(ApartmentState.STA);
t.Start();
}

private void CreateControls()
{
Button btn = new Button();
btn.Width = 90;
btn.Height = 30;
btn.Background = Brushes.Black;
btn.Foreground = Brushes.White;
btn.Content = "Click me";

this.Dispatcher.BeginInvoke((Action)delegate
{
Dispatcher.Invoke((Action)(() =>
{
MainGD.Children.Add(btn);
}));
});
}

At MainGD.Children.Add(btn); I receive: The calling thread cannot access this object because a different thread owns it.


In my search, I found the method of put the button creation inside the Invoke, like this:


this.Dispatcher.BeginInvoke((Action)delegate
{
Dispatcher.Invoke((Action)(() =>
{
Button btn = new Button();
btn.Width = 90;
btn.Height = 30;
btn.Background = Brushes.Black;
btn.Foreground = Brushes.White;
btn.Content = "Click me";

MainGD.Children.Add(btn);
}));
});

But I don't want to cread the control in the main thread.
Could anyone help me with this?
Is that possible at all?




The point is: Is it possible to create a GUI control inside another thread and transfer it to the GUI thread? That was what I was trying to do.




Nice idea, but that won't work I'm afraid. User interface code must go on the GUI thread.


BackgroundWorkers and threads are for doing work and the GUI thread is for doing user interface. "Work" is things like number-crunching or database lookup: things which will eventually send their results to the GUI thread for display.


If your form takes too long to load purely from adding controls to it then there's too many controls, probably - see if you can redesign it.




Since all of the used UI objects must be created on the UI thread, you will have to resort to your second example. However, you can still control the behavior. Consider the following solution (Note the DispatcherPriority on the bottom of the BeginInvoke call):


    public MainWindow()
{
this.Loaded += MainWindow_Loaded;
}

private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
Thread loadControlsThread = new Thread(new ThreadStart(LoadControls_ThreadProc));
loadControlsThread.Priority = ThreadPriority.BelowNormal;
loadControlsThread.IsBackground = true;
loadControlsThread.Start();
}

private void LoadControls_ThreadProc()
{
this.Dispatcher.BeginInvoke((Action)(() =>
{
Button btn = new Button();
btn.Width = 90;
btn.Height = 30;
btn.Background = Brushes.Black;
btn.Foreground = Brushes.White;
btn.Content = "Click me";

MainGD.Children.Add(btn);
}), System.Windows.Threading.DispatcherPriority.Loaded, null);
}



Sounds like async/await may be of use here. I worked on a project where our Window UI consisted of some menu items on the top, a region of custom controls in the middle, and some action buttons (like Ok, Cancel) on the bottom. The menu items and action buttons are created when the Window is created. However, the controls in the middle region were generated using asynchronous calls.


We used an ObservableCollection as the data source for an ItemsControl in the middle region. Then, in the Window.ContentRendered event (so after the window has been drawn) we "awaited" a method that built the custom controls.


Here's a snippet of MyWindow code-behind:


public partial class MyWindow : Window
{
/* Window c'tor, other methods/props ommited */

// Load our custom controls asynchronously after the window is rendered
private async void MyWindow_ContentRendered(object sender, EventArgs e)
{
// kick off the asynchronous call to create a custom controls
await CreateCustomControls();
}

// Asynchronously adds MyCustomControls to the ObservableCollection
// (we actually used a separate static class that contained our definitions
// and created the controls)
private async Task CreateCustomControls()
{
// the def describes what you want to build and is not detailed here
foreach (var controlDef in MyCustomControlDefinitions)
{
// create a control based on the definition
var newControl = await CreateMyCustomControl(controlDef);

// Don't add the control until it is completely built
MyCustomControls.Add(newControl);
}
}

//The (bindable) collection of custom controls
public ObservableCollection<MyCustomControl> MyCustomControls
{
get { return (ObservableCollection<MyCustomControl>)GetValue(MyCustomControlsProperty); }
set { SetValue(MyCustomControlsProperty, value); }
}

public static readonly DependencyProperty MyCustomControlsProperty =
DependencyProperty.Register("MyCustomControls", typeof(ObservableCollection<MyCustomControl>),
typeof(MyWindow), new PropertyMetadata(new ObservableCollection<MyCustomControl>()));

}

Here's a snippet of the MyWindow XAML:


<Window>
<Grid>
<!-- Scrollable Panel for MyCustomControls -->
<ScrollViewer>
<ItemsControl ItemsSource="{Binding MyCustomControls}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer>
</Grid>
</Window>

Its important to note that it only provides a "perceived" performance increase. All we did was allow the window to be somewhat useful while we creating the rest of the controls. So users could interact with the top and bottom controls immediately (when the window is shown) and once the custom controls were added to the ObservableCollection, the user could interact with them as well.


Hope this helps.



The more a window is filled with content, more it will delay to load completely, and the user will be able to use any of the controls only when it have already finished.


So I imagined the following:


Making a window empty of controls and create them on another thread, like BackgroundWorker. Then, the controls would be added to the main grid gradually, so the user could use the first controls without have to wait the rest to be created.


To try to bring it to reality, I've already tried to use some techniques. The last one was trying to use Invoke, which, actually, I don't know neither how that works nor how to use.


public MainWindow()
{
InitializeComponent();

Thread t = new Thread(new ThreadStart(CreateControls));
t.SetApartmentState(ApartmentState.STA);
t.Start();
}

private void CreateControls()
{
Button btn = new Button();
btn.Width = 90;
btn.Height = 30;
btn.Background = Brushes.Black;
btn.Foreground = Brushes.White;
btn.Content = "Click me";

this.Dispatcher.BeginInvoke((Action)delegate
{
Dispatcher.Invoke((Action)(() =>
{
MainGD.Children.Add(btn);
}));
});
}

At MainGD.Children.Add(btn); I receive: The calling thread cannot access this object because a different thread owns it.


In my search, I found the method of put the button creation inside the Invoke, like this:


this.Dispatcher.BeginInvoke((Action)delegate
{
Dispatcher.Invoke((Action)(() =>
{
Button btn = new Button();
btn.Width = 90;
btn.Height = 30;
btn.Background = Brushes.Black;
btn.Foreground = Brushes.White;
btn.Content = "Click me";

MainGD.Children.Add(btn);
}));
});

But I don't want to cread the control in the main thread.
Could anyone help me with this?
Is that possible at all?




The point is: Is it possible to create a GUI control inside another thread and transfer it to the GUI thread? That was what I was trying to do.



Nice idea, but that won't work I'm afraid. User interface code must go on the GUI thread.


BackgroundWorkers and threads are for doing work and the GUI thread is for doing user interface. "Work" is things like number-crunching or database lookup: things which will eventually send their results to the GUI thread for display.


If your form takes too long to load purely from adding controls to it then there's too many controls, probably - see if you can redesign it.



Since all of the used UI objects must be created on the UI thread, you will have to resort to your second example. However, you can still control the behavior. Consider the following solution (Note the DispatcherPriority on the bottom of the BeginInvoke call):


    public MainWindow()
{
this.Loaded += MainWindow_Loaded;
}

private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
Thread loadControlsThread = new Thread(new ThreadStart(LoadControls_ThreadProc));
loadControlsThread.Priority = ThreadPriority.BelowNormal;
loadControlsThread.IsBackground = true;
loadControlsThread.Start();
}

private void LoadControls_ThreadProc()
{
this.Dispatcher.BeginInvoke((Action)(() =>
{
Button btn = new Button();
btn.Width = 90;
btn.Height = 30;
btn.Background = Brushes.Black;
btn.Foreground = Brushes.White;
btn.Content = "Click me";

MainGD.Children.Add(btn);
}), System.Windows.Threading.DispatcherPriority.Loaded, null);
}


Sounds like async/await may be of use here. I worked on a project where our Window UI consisted of some menu items on the top, a region of custom controls in the middle, and some action buttons (like Ok, Cancel) on the bottom. The menu items and action buttons are created when the Window is created. However, the controls in the middle region were generated using asynchronous calls.


We used an ObservableCollection as the data source for an ItemsControl in the middle region. Then, in the Window.ContentRendered event (so after the window has been drawn) we "awaited" a method that built the custom controls.


Here's a snippet of MyWindow code-behind:


public partial class MyWindow : Window
{
/* Window c'tor, other methods/props ommited */

// Load our custom controls asynchronously after the window is rendered
private async void MyWindow_ContentRendered(object sender, EventArgs e)
{
// kick off the asynchronous call to create a custom controls
await CreateCustomControls();
}

// Asynchronously adds MyCustomControls to the ObservableCollection
// (we actually used a separate static class that contained our definitions
// and created the controls)
private async Task CreateCustomControls()
{
// the def describes what you want to build and is not detailed here
foreach (var controlDef in MyCustomControlDefinitions)
{
// create a control based on the definition
var newControl = await CreateMyCustomControl(controlDef);

// Don't add the control until it is completely built
MyCustomControls.Add(newControl);
}
}

//The (bindable) collection of custom controls
public ObservableCollection<MyCustomControl> MyCustomControls
{
get { return (ObservableCollection<MyCustomControl>)GetValue(MyCustomControlsProperty); }
set { SetValue(MyCustomControlsProperty, value); }
}

public static readonly DependencyProperty MyCustomControlsProperty =
DependencyProperty.Register("MyCustomControls", typeof(ObservableCollection<MyCustomControl>),
typeof(MyWindow), new PropertyMetadata(new ObservableCollection<MyCustomControl>()));

}

Here's a snippet of the MyWindow XAML:


<Window>
<Grid>
<!-- Scrollable Panel for MyCustomControls -->
<ScrollViewer>
<ItemsControl ItemsSource="{Binding MyCustomControls}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer>
</Grid>
</Window>

Its important to note that it only provides a "perceived" performance increase. All we did was allow the window to be somewhat useful while we creating the rest of the controls. So users could interact with the top and bottom controls immediately (when the window is shown) and once the custom controls were added to the ObservableCollection, the user could interact with them as well.


Hope this helps.


0 commentaires:

Enregistrer un commentaire