I'm working on a multithreaded application were a new "closable" tab is opened for each new thread. I got the code for closable tabitems from this site but I also want to have a textbox in the tabitem. I tired adding the textbox during runtime from the main method, but it was not accessible from the thread which was created after. what is the best way to make this work? I'm looking for the best way to add a textbox to the closable tabs which I can edit from other worker threads.
EDIT:
I have added some sample code to show what I'm trying to achieve.
namespace SampleTabControl
{
public partial class Window1 : Window
{
public static Window1 myWindow1;
public Window1()
{
myWindow1 = this;
InitializeComponent();
this.AddHandler(CloseableTabItem.CloseTabEvent, new RoutedEventHandler(this.CloseTab));
}
private void CloseTab(object source, RoutedEventArgs args)
{
TabItem tabItem = args.Source as TabItem;
if (tabItem != null)
{
TabControl tabControl = tabItem.Parent as TabControl;
if (tabControl != null)
tabControl.Items.Remove(tabItem);
}
}
private void btnAdd_Click(object sender, RoutedEventArgs e)
{
Worker worker = new Worker();
Thread[] threads = new Thread[1];
for (int i = 0; i < 1; i++)
{
TextBox statusBox = new TextBox();
CloseableTabItem tabItem = new CloseableTabItem();
tabItem.Content = statusBox;
MainTab.Items.Add(tabItem);
int index = i;
threads[i] = new Thread(new ParameterizedThreadStart(worker.start));
threads[i].IsBackground = true;
threads[i].Start(tabItem);
}
}
}
}
And this is the Worker class.
namespace SampleTabControl
{
class Worker
{
public CloseableTabItem tabItem;
public void start(object threadParam)
{
tabItem = (CloseableTabItem)threadParam;
Window1.myWindow1.Dispatcher.BeginInvoke((Action)(() => { tabItem.Header = "TEST"; }), System.Windows.Threading.DispatcherPriority.Normal);
//Window1.myWindow1.Dispatcher.BeginInvoke((Action)(() => { tabItem.statusBox.Text //statusbox is not accesible here= "THIS IS THE TEXT"; }), System.Windows.Threading.DispatcherPriority.Normal);
while (true)
{
Console.Beep();
Thread.Sleep(1000);
}
}
}
}
In the line which I have commented out, statusBox is not accessible.
After seeing your edit, it is clear my original post was not answering the original question.
I think to access the textbox in the way you want you need to cast the tabItem.Content to a Textbox.
Something like below could work
TextBox t = tabItem.Content as TextBox;
if (t != null)
Window1.myWindow1.Dispatcher.BeginInvoke((Action)(() => { t.Text = "THIS IS THE TEXT";}), System.Windows.Threading.DispatcherPriority.Normal);
WPF cannot modify items that were created on a different thread then the current one
If you haven't already, I would highly recommend that you look into the MVVM design pattern. This separates the UI layer from the business logic layer. Your application becomes your ViewModel classes, and the UI layer (Views) are simply a pretty interface that allows users to easily interact with the ViewModels.
This means that all your UI components would share a single thread, while your longer running processes such as retrieving data or crunching numbers can all safely be done on background threads.
For example, you could bind your TabControl.ItemsSource to an ObservableCollection<TabViewModels>, and when you execute the AddTabCommand you would start a new background worker to add a new TabViewModel to the MainViewModel.TabViewModels collection.
Once the background worker finishes it's job. the UI gets automtaically notified that there is a new item in the collection and will draw the new TabItem in the TabControl for you, using whatever DataTemplate you specify.
Related
I have recently made a Class Library (dll) for my other project to program a Bluetooth device via serial port (COM). The library is used to transfer firmware via COM port. It works fine until the requirement comes, which requires a WPF window to show the progress of programming. I have successfully created the progress bar using standard WPF app template. However, the standard WPF does not allow me to generate dll. After searching here, I found this link that teaches you how to add a WPF window to existing Class Library project. Also, someone teaches you how to show the window from here. Everything look good until I tried, there is nothing shows up when I call the method ProgrammBluetooth() from LabVIEW.
My main method, which is in a separate .cs file:
namespace BTMProg
{
public class BTMProgrammer
{
private bool _uut1Status = false;
private string _uut1Message = "";
public bool UUT1Status
{
get { return _uut1Status; }
set { _uut1Status = value; }
}
public string UUT1Message
{
get { return _uut1Message; }
set { _uut1Message = value; }
}
public void ProgramBluetooth (string ioPort, string firmwareFile)
{
List<UUT> uutList = new List<UUT>();
uutList.Add(new UUT(ioPort, "UUT1", 1));
Thread thread = new Thread(() =>
{
var wn = new MainWindow(uutList, firmwareFile);
wn.ShowDialog();
wn.Closed += (s, e) => wn.Dispatcher.InvokeShutdown();
Dispatcher.Run();
if (wn.TaskList[0].Result.ToUpper().Contains("SUCCESS"))
{
_uut1Status = true;
_uut1Message = wn.TaskList[0].Result;
}
else
{
_uut1Status = false;
_uut1Message = wn.TaskList[0].Result;
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
}
}
}
My WPF code in MainWindow.xaml.cs:
ProgrammingViewModel _pvm = new ProgrammingViewModel();
private List<string> _viewModeList = new List<string>();
private List<Task<string>> _taskList = new List<Task<string>>();
public List<Task<string>> TaskList {
get => _taskList;
set => _taskList = value;
}
public MainWindow(List<UUT> uutList, string firmwareFile)
{
InitializeComponent();
foreach (var uut in uutList)
{
_viewModeList.Add(uut.UutName);
}
_pvm.AddProcessViewModels(_viewModeList);
ProgressBarView.DataContext = _pvm.ProcessModels;
StartProgramming(uutList, firmwareFile);
Application.Current.MainWindow.Close();
}
The issue before was that if I don't use dispatcher to create a new thread, an exception saying "The calling thread must be STA, because many UI components require this...." thrown. After I use the new thread, no error but the window does not show up as expected. What could be the problem? Thanks.
The ShowDialog function will stop execution of the thread until the window closes, meaning the rest of that code may not run and the dispatcher may not be started. You should try the Show method instead, which returns as soon as the window is shown.
Also, what is going on with these lines in the constructor of the window?
StartProgramming(uutList, firmwareFile);
Application.Current.MainWindow.Close();
Whatever that first line does, it needs to return and not do a bunch of work if you want the window to finish getting constructed. The second line makes no sense at all. Why are you closing the main window of the application? Did you even set and open a window associated with that property at some point?
I suspect one or more of these things is preventing the thread from ever reaching the point where it can show the window.
I set a WPF ProgressBar's Is indeterminate to True and Visibility to Hidden. In a event handler I am trying to make the ProgressBar visible while a ObservableCollection is being updated (and a ListView whose ItemsSource is the ObservableCollection). I hope DoEvents() from System.Windows.Forms.Application can make it visible but it does not.
I notice SetPBarHelper(() => { ..} ) usually finish much earlier than the ListView shows visual changes.
How do I make the ProgressBar Visible inside event handler codes ?
How to tell if my ListView is still being updated even though the ObserableCollection has finished adding items ?
<ProgressBar x:Name="GeneralProgressBar" Width="300" Height="15" IsIndeterminate="True" Visibility="Hidden"/>
private void SetPBar(bool isVisible)
{
if (isVisible)
GeneralProgressBar.Visibility = System.Windows.Visibility.Visible;
else
GeneralProgressBar.Visibility = System.Windows.Visibility.Hidden;
}
private void SetPBarHelper(Action handler)
{
SetPBar(true); // try to make ProgressBar visible
System.Windows.Forms.Application.DoEvents();
handler(); // use the event handling, which run database query
SetPBar(false); // try to make ProgressBar disappear
}
private void CommandForumImagesBinding_Executed(object sender, ExecutedRoutedEventArgs e)
{
SetPBarHelper(() =>
{
if (e.Parameter == null)
return;
var vm = e.Parameter as ForumViewModel;
if (vm != null)
{
}
});
}
public sealed class ImageGroupCollection : ObservableCollection<ImageGroup>
{
public ImageGroupCollection() : base() { }
public void Update(DateTime start, DateTime end)
{
ClearItems();
var list = MyDatabase.GetRecords(start, end);
if (list != null)
{
foreach (var g in list)
{
Add(g);
}
}
}
}
Your problem is simply because you are blocking the UI thread. You can't do that.
By the time the UI thread is running the message loop again you have set ProgressBar.Visible = false. The ProgressBar is never drawn.
Assuming you are using .net 4.5 You need to rewrite the code as follows.
private async Task SetPBarHelper(Action handler)
{
SetPBar(true); // try to make ProgressBar visible
System.Windows.Forms.Application.DoEvents();
await Task.Run(handler); // use the event handling, which run database query
SetPBar(false); // try to make ProgressBar disappear
}
But overall you need to return control of the UI thread back to the application loop as soon as possible to allow it to redraw the windows, and run the update on a WorkerThread.
I populate a a DataGridControl from the WPF Extended Toolkit with DataTables populated on user selection. When user selection is modified, there is not only a large delay with displaying a DataTable with 200 x 1000 rows, there is a user interaction delay that prevents users from interacting with other controls, even though the population of the DataTable binded to the DataGridControl occurs on a separate thread.
How do I remove the delay so that the user may interact with other controls in a View while the DataGridControl is updating?
public string ListBoxSelection {
get {
return listBoxSelection;
}
set {
listBoxSelection = value;
OnPropertyChanged("ListBoxSelection"); // DataGridSelection
BackgroundWorker threadPreviewLoader = new BackgroundWorker();
threadPreviewLoader.DoWork += (LoadDataGridPreview);
threadPreviewLoader.RunWorkerAsync();
}
}
private DataTable dTPreviewWindow; //modified in thread
public DataView dvLbSelection {
get {
return dTPreviewWindow.DefaultView; //DataGridControl binding
}
}
private void LoadDataGridPreview(object sender, DoWorkEventArgs e) {
if (listBoxSelection != null) {
try {
DataTable testImmediateChange = new DataTable();
testImmediateChange = DataSetModel.ChunkFlatFile(listBoxSelection, 1, PREVIEW_WINDOW_MAX_ROWS); //labor itensive work
dTPreviewWindow = testImmediateChange;
fileOpenGood = true;
} catch {
fileOpenText = DATAGRID_TEXT_BADFILE;
fileOpenGood = false;
}
fileOpenText = DATAGRID_TEXT_NOFILECHOSEN;
OnPropertyChanged("FileOpenGood");
OnPropertyChanged("FileOpenBad");
OnPropertyChanged("FileOpenText");
OnPropertyChanged("dvLbSelection");
} else {
ValidatePreviewWindow(true);
}
}
If your dataview i.e. dvLbSelection is UI bound, even updating it in the background thread would cause the UI to refresh. And which in turn would cause delays when user is trying to interact with other controls. So here you can try couple of things.
Disconnect the dataview from the UI until all data is loaded. (Use temp variable to load the values in background thread)
Use Virtualization on the data grid and set the VirtualizingStackPanel.VirtualizationMode ="Recycling"
Avoid creating multiple background threads (as per the provided code snippet, for every ListBox Selection a new thread is getting created). Running expensive operations with many thread would be one of the primary reason for the delays.
Hope this helps!
I have a Winforms Application with a TabStrip Control. During runtime, UserControls are to be loaded into different tabs dynamically.
I want to present a "User Control xyz is loading" message to the user (setting an existing label to visible and changing its text) before the UserControl is loaded and until the loading is completely finished.
My approaches so far:
Trying to load the User Control in a BackgroundWorker thread. This fails, because I have to access Gui-Controls during the load of the UserControl
Trying to show the message in a BackgroundWorker thread. This obviously fails because the BackgroundWorker thread is not the UI thread ;-)
Show the Message, call DoEvents(), load the UserControl. This leads to different behaviour (flickering, ...) everytime I load a UserControl, and I can not control when and how to set it to invisible again.
To sum it up, I have two questions:
How to ensure the message is visible directly, before loading the User control
How to ensure the message is set to invisible again, just in the moment the UserControl is completely loaded (including all DataBindings, grid formattings, etc.)
what we use is similar to this:
create a new form that has whatever you want to show the user,
implement a static method where you can call this form to be created inside itself, to prevent memory leaks
create a new thread within this form so that form is running in a seperated thread and stays responsive; we use an ajax control that shows a progress bar filling up.
within the method you use to start the thread set its properties to topmost true to ensure it stays on top.
for instance do this in your main form:
loadingForm.ShowLoadingScreen("usercontrollname");
//do something
loadingform.CloseLoadingScreen();
in the loading form class;
public LoadingScreen()
{
InitializeComponent();
}
public static void ShowLoadingScreen(string usercontrollname)
{
// do something with the usercontroll name if desired
if (_LoadingScreenThread == null)
{
_LoadingScreenThread = new Thread(new ThreadStart(DoShowLoadingScreen));
_LoadingScreenThread.IsBackground = true;
_LoadingScreenThread.Start();
}
}
public static void CloseLoadingScreen()
{
if (_ls.InvokeRequired)
{
_ls.Invoke(new MethodInvoker(CloseLoadingScreen));
}
else
{
Application.ExitThread();
_ls.Dispose();
_LoadingScreenThread = null;
}
}
private static void DoShowLoadingScreen()
{
_ls = new LoadingScreen();
_ls.FormBorderStyle = FormBorderStyle.None;
_ls.MinimizeBox = false;
_ls.ControlBox = false;
_ls.MaximizeBox = false;
_ls.TopMost = true;
_ls.StartPosition = FormStartPosition.CenterScreen;
Application.Run(_ls);
}
Try again your second approach:
Trying to show the message in a BackgroundWorker thread. This obviously fails because the BackgroundWorker thread is not the UI thread ;-)
But this time, use the following code in your background thread in order to update your label:
label.Invoke((MethodInvoker) delegate {
label.Text = "User Control xyz is loading";
label.Visible = true;
});
// Load your user control
// ...
label.Invoke((MethodInvoker) delegate {
label.Visible = false;
});
Invoke allows you to update your UI in another thread.
Working from #wterbeek's example, I modified the class for my own purposes:
center it over the loading form
modification of its opacity
sizing it to the parent size
show it as a dialog and block all user interaction
I was required to show a throbber
I received a null error on line:
if (_ls.InvokeRequired)
so I added a _shown condition (if the action completes so fast that the _LoadingScreenThread thread is not even run) to check if the form exists or not.
Also, if the _LoadingScreenThread is not started, Application.Exit will close the main thread.
I thought to post it for it may help someone else. Comments in the code will explain more.
public partial class LoadingScreen : Form {
private static Thread _LoadingScreenThread;
private static LoadingScreen _ls;
//condition required to check if the form has been loaded
private static bool _shown = false;
private static Form _parent;
public LoadingScreen() {
InitializeComponent();
}
//added the parent to the initializer
//CHECKS FOR NULL HAVE NOT BEEN IMPLEMENTED
public static void ShowLoadingScreen(string usercontrollname, Form parent) {
// do something with the usercontroll name if desired
_parent = parent;
if (_LoadingScreenThread == null) {
_LoadingScreenThread = new Thread(new ThreadStart(DoShowLoadingScreen));
_LoadingScreenThread.SetApartmentState(ApartmentState.STA);
_LoadingScreenThread.IsBackground = true;
_LoadingScreenThread.Start();
}
}
public static void CloseLoadingScreen() {
//if the operation is too short, the _ls is not correctly initialized and it throws
//a null error
if (_ls!=null && _ls.InvokeRequired) {
_ls.Invoke(new MethodInvoker(CloseLoadingScreen));
} else {
if (_shown)
{
//if the operation is too short and the thread is not started
//this would close the main thread
_shown = false;
Application.ExitThread();
}
if (_LoadingScreenThread != null)
_LoadingScreenThread.Interrupt();
//this check prevents the appearance of the loader
//or its closing/disposing if shown
//have not found the answer
//if (_ls !=null)
//{
_ls.Close();
_ls.Dispose();
//}
_LoadingScreenThread = null;
}
}
private static void DoShowLoadingScreen() {
_ls = new LoadingScreen();
_ls.FormBorderStyle = FormBorderStyle.None;
_ls.MinimizeBox = false;
_ls.ControlBox = false;
_ls.MaximizeBox = false;
_ls.TopMost = true;
//get the parent size
_ls.Size = _parent.Size;
//get the location of the parent in order to show the form over the
//target form
_ls.Location = _parent.Location;
//in order to use the size and the location specified above
//we need to set the start position to "Manual"
_ls.StartPosition =FormStartPosition.Manual;
//set the opacity
_ls.Opacity = 0.5;
_shown = true;
//Replaced Application.Run with ShowDialog to show as dialog
//Application.Run(_ls);
_ls.ShowDialog();
}
}
I am trying to make a warning window in an application. The window needs to run on a seperate thread and contains among other things a Canvas depicting a failing object. The Canvas already exists in the main application, and what i need is simply to show the same Canvas in the warning window. The problem is that i get an error saying that another thread owns the object.
I tried doing a deep copy using this method but with no luck. Is there anything i missed, or is there really no simple method to copy a Canvas, or a collection of images. Alternatively, would it be possible to do the deep copy and then change the treading affinity of the copied object?
I should think that someone has encountered this problem before, but my serching skills have given me no relevant results this time.
Thanks in advance!
-ruNury
EDIT 1
private Canvas cloneCanvas()
{
Canvas testcanv = new Canvas();
Dispatcher.Invoke(new Action(delegate
{
var t = SomeViewModel.GetCanvasWithImages();
testcanv = CopyCanvas(t);
}));
return testcanv;
}
public static UIElement DeepCopy(UIElement element)
{
if (element != null)
{
var xaml = XamlWriter.Save(element);
var xamlString = new StringReader(xaml);
var xmlTextReader = new XmlTextReader(xamlString);
var deepCopyObject = (UIElement)XamlReader.Load(xmlTextReader);
return deepCopyObject;
}
return null;
}
private Canvas CopyCanvas(Canvas inputCanvas)
{
if (inputCanvas != null)
{
var outputCanvas = new Canvas();
foreach (UIElement child in inputCanvas.Children)
{
outputCanvas.Children.Add(DeepCopy(child));
}
return outputCanvas;
}
return null;
}
You can singleton pattern to maintain one object of warning window.
When you want to put the canvas into warning window, you have to use Dispatcher.
Dispatcher will marshal method call onto UI thread.
something like
warningWindow.Dispatcher.Invoke(
System.Windows.Threading.DispatcherPriority.Normal,
new Action(
delegate()
{
myCheckBox.IsChecked = true;
}
));
Where warningWindow will be available through singleton instance