Accesing WPF window properties from Web Forms page - c#

I have a scenario, in witch i need to open new WPF window form Web Forms page, and ten bind to properities in that window and display it in update panel, so the data on web page, changes with user input in WPF window.
I tried something like this:
public partial class WorkerPanel : System.Web.UI.Page
{
private MainWindow _mainWindow;
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
_mainWindow = new MainWindow();
_mainWindow.Show();
}
}
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
rptTransactions.DataSource = _mainWindow.Distributors;
rptTransactions.DataBind();
}
}
But it gives me the following error:
The calling thread must be STA, because many UI components require this.
Accoridingly to this question https://stackoverflow.com/questions/2329978 i changed my code to:
Thread t = new Thread(() =>
{
_mainWindow = new MainWindow();
_mainWindow.Show();
System.Windows.Threading.Dispatcher.Run();
});
t.SetApartmentState(ApartmentState.STA);
t.IsBackground = true;
t.Start();
With this, my web page and WPF window loads fine, but i cannot bind to this window properties, as it runs in new thread. Is such binding even possible or I should take different approach ?

I managed to do it my way using callbacks. For anyone with the same problem here's what i did:
First i added a delegate in my WPF window class
public delegate void DistributorsDataCallback(List<DistributorHandler> distributors);
private DistributorsDataCallback _callback;
then i created a new constructor for my window accepting this delegate as a parameter
public MainWindow(DistributorsDataCallback callbackDelegate)
{
InitializeComponent();
InitializeDistributors();
_callback = callbackDelegate;
}
and somwhere in code i call it with data i want to pass
_callback(Distributors);
On my Web Forms page:
Thread t = new Thread(() =>
{
MainWindow _mainWindow = new MainWindow(GetDistributorsData);
_mainWindow.Show();
System.Windows.Threading.Dispatcher.Run();
});
t.SetApartmentState(ApartmentState.STA);
t.IsBackground = true;
t.Start();
And now data can be accesed without any problems in GetDistributorsData function.
This solution should work in any multi-thread application.

Related

How to update UI in separate thread?

I have a WPF view with grid that have two elements in it, RichText and Progressbar.
When I'm loading a lot of text to RichText I want to show a loading process (just an animation) to the user.
The main idea to hide Richtext control, show Progressbar, start load text, when it finish show RichText again.
The problem is that while I'm updating RichText control I'm blocking UI and Progressbar is freezed.
Is there a way to update Progressbar from another thread, maybe some proxyhost?
Thank you.
Is there a way to update Progressbar from another thread
Short answer: No. A control can only be updated on the thread on which it was originally created.
What you can do is to display the ProgressBar in another window that runs on another thread and then close this window when the RichTextBox on the original thread has been updated.
You can add this to your usings:
using System.Windows.Threading;
For .NET 5/6, that is enough. For .NET framework you must also add a reference to System.Windows.Presentation.dll.
And this type of code will work fine:
private void Button_Click(object sender, RoutedEventArgs e)
{
// here we're in UI thread
var max = 100;
pg.Maximum = max; // pg is a progress bar
Task.Run(() =>
{
// here we're in another thread
for (var i = 0; i < max; i++)
{
Thread.Sleep(100);
// this needs the System.Windows.Threading using to support lambda expressions
Dispatcher.BeginInvoke(() =>
{
// this will run in UI thread
pg.Value = i;
});
}
});
}
Create a method on your form to update the element you want to update and use invoke to run it from your thread
like that:
private void Form1_Load(object sender, EventArgs e)
{
Thread _thread = new Thread(() =>
{
//do some work
upDateUiElements();//pass any parameters you want
});
_thread.Start();
}
public void upDateUiElements()//input any parameters you need
{
BeginInvoke(new MethodInvoker(() =>
{
//update ui elements
}));
}
If you need to invoke it from a different class you can pass your form as an object and then access the method through that object
Like that:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
OtherClass _class = new OtherClass(this);
_class.runthread();
}
public void upDateUiElements()//input any parameters you need
{
BeginInvoke(new MethodInvoker(() =>
{
//update ui elements
}));
}
}
class OtherClass
{
private Form1 _accessForm1;
public OtherClass(Form1 accessform1)
{
_accessForm1 = accessform1;
}
public void runthread()
{
Thread _thread = new Thread(() =>
{
//do some work
_accessForm1.upDateUiElements();
});
_thread.Start();
}
}

e.NewWindow = (CoreWebView2)sender still results in a separate instance

I want to navigate to the URL rather than it opening a separate instance.
No matter what I do it still opens another instance of WebView2.
private void CoreWebView2_NewWindowRequested(object sender,
CoreWebView2NewWindowRequestedEventArgs e)
{
//e.NewWindow = webView21.CoreWebView2;
e.NewWindow = (CoreWebView2)sender;
//e.Handled = true;
}
here's the original post, what do I need to do for it to handle the new window request?
To receive a notification when a new Popup Windows is requested, subscribe to the the NewWindowRequested of CoreWebView2.
The event is raised when there's a request to generate a Popup. Clicking a link that just cause the Browser to navigate to a different URI doesn't raise the event (no popup).
A new Popup Window can be requested if the User clicks the Open link in new window (sic) option of the standard ContextMenu provided by the Browser.
Or if the Web Page generates one without user intervention.
Unfortunately, the e.IsUserInitiated property is always true, so you may have a hard time determining (without injecting JavaScripts) whether the popup should be blocked (in case you want to, that is).
When a new Window is requested, you can block it indiscriminately setting e.Handled = true
If you want to open the new Window URI to the same Window, you can specify either:
e.Handled = true;
e.NewWindow = (CoreWebView2)sender;
// or
e.Handled = true;
((CoreWebView2)sender).Navigate(e.Uri);
Sample WebView2 main handler Form:
using Microsoft.Web.WebView2.Core;
public partial class MainForm : Form
{
public MainForm() => InitializeComponent();
protected override async void OnLoad(EventArgs e)
{
base.OnLoad(e);
webView2.CoreWebView2InitializationCompleted += OnCoreWebView2InitializationCompleted;
var env = await CoreWebView2Environment.CreateAsync(null, null, null);
await webView2.EnsureCoreWebView2Async(env);
webView2.Source = new Uri("https://www.somesite.com");
}
private void OnCoreWebView2InitializationCompleted(object sender, CoreWebView2InitializationCompletedEventArgs e)
{
webView2.CoreWebView2.NewWindowRequested += OnNewWindowRequested;
}
private void OnNewWindowRequested(object sender, CoreWebView2NewWindowRequestedEventArgs e)
{
// Open the Uri requested in the current Window
e.Handled = true;
((CoreWebView2)sender).Navigate(e.Uri);
}
// Or, if you want to handle Popup Windows using your own Form template
// => Note that it's the same event handler as above, pick one, not both!
private void OnNewWindowRequested(object sender, CoreWebView2NewWindowRequestedEventArgs e)
{
// Open the Uri requested in a new instance of the PopupWindow Form
var deferral = e.GetDeferral();
e.Handled = true;
var popup = new PopupWindow(deferral, e);
popup.Show();
}
}
If you instead want to create a new Form that will show the popup, you need a Form template (it can be just a Form that contains a WebView2 Control) that receive the CoreWebView2Deferral returned by e.GetDeferral().
In the initialization procedure of this Form check whether the CoreWebView2Deferral object is null. If it's not, complete the deferred event by calling its Complete() method.
Then subscribe to the NewWindowRequested event to perform the same action when a new Popup Window is requested (unless you want to block it).
Of course you can show these Forms inside a Tabbed Control, to generate a standard tabbed view, common in all WebBrowsers.
Sample PopupWindow Form:
using Microsoft.Web.WebView2.Core;
using System.Windows.Forms;
public partial class PopupWindow : Form
{
public PopupWindow() => InitializeComponent();
public PopupWindow(CoreWebView2Deferral deferral, CoreWebView2NewWindowRequestedEventArgs args)
: this() {
Core2Deferral = deferral;
NewWindowArgs = args;
}
protected virtual CoreWebView2Deferral Core2Deferral { get; private set; }
protected virtual CoreWebView2NewWindowRequestedEventArgs NewWindowArgs { get; private set; }
protected async override void OnLoad(EventArgs e)
{
base.OnLoad(e);
webView2.CoreWebView2InitializationCompleted += OnCoreWebView2InitializationCompleted;
var env = await CoreWebView2Environment.CreateAsync(null, null, null);
await webView2.EnsureCoreWebView2Async(env);
}
private void OnCoreWebView2InitializationCompleted(object sender, CoreWebView2InitializationCompletedEventArgs e)
{
webView2.CoreWebView2.Settings.AreDefaultContextMenusEnabled = true;
if (Core2Deferral != null) {
NewWindowArgs.NewWindow = webView2.CoreWebView2;
Core2Deferral.Complete();
}
webView2.CoreWebView2.NewWindowRequested += OnNewWindowRequested;
}
private void OnNewWindowRequested(object sender, CoreWebView2NewWindowRequestedEventArgs e)
{
e.Handled = true;
var popup = new PopupWindow(e.GetDeferral(), e);
popup.Show();
}
}
How about making it works like real web browsers. Simply, opens in an new Tab. Just with a few lines of codes ;)
At first, create a new class which inherits from WebView2 and include a TabControl field inside it:
internal class WebViewInTab:WebView2
{
TabControl tabCtrl;
public WebViewInTab(TabControl tabCtrl) :base()
{
Dock = DockStyle.Fill; // necessary for showing
this.tabCtrl = tabCtrl; // for adding new TabPage controls
CoreWebView2InitializationCompleted += WebViewInTab_CoreWebView2InitializationCompleted;
}
Then, you'll use this custom webview2 to create new webview2 objects every time CoreWebView2.NewWindowRequested event is raised. So to handle the event:
private void WebViewInTab_CoreWebView2InitializationCompleted(object? sender, Microsoft.Web.WebView2.Core.CoreWebView2InitializationCompletedEventArgs e)
{
CoreWebView2.NewWindowRequested += CoreWebView2_NewWindowRequested; // This is the man
CoreWebView2.DocumentTitleChanged += CoreWebView2_DocumentTitleChanged; // Just cosmetic code
}
After that, you'll handle the new window by yourself, i.e. , Just add a new TabPage control with our custom webview2 to the TabControl TabPages collection. Of course, not forgeting the Uri from handler arg.
private void CoreWebView2_NewWindowRequested(object? sender, Microsoft.Web.WebView2.Core.CoreWebView2NewWindowRequestedEventArgs e)
{
e.Handled = true; // let the default new window
TabPage tpage = new TabPage(); // boy
tpage.Controls.Add(new WebViewInTab(tabCtrl) { Source = new Uri(e.Uri)}); // toy
tabCtrl.TabPages.Add(tpage); // daddy
tabCtrl.SelectedTab = tpage; // user expectation
}
//Just cosmetic code
private void CoreWebView2_DocumentTitleChanged(object? sender, object e)
{
int last = tabCtrl.TabPages.Count - 1;
tabCtrl.TabPages[last].Text = CoreWebView2.DocumentTitle
}
}
Finally, :) start the recursion-ready operation in the main app form constructor.
public Form1()
{
InitializeComponent();
string uriAdd = "https://www.w3schools.com/";
tabControl1.TabPages[0].Controls.Add(new WebViewInTab(tabControl1) { Source = new Uri(uriAdd) });
}

GUI and server application

I have a question regarding WPF and server application. I was given a task to write a simple TCP server with a GUI. I'm new to C# (and GUIs in general), hence I have a question.
I have 2 classes:
App.xaml.cs
public partial class App : Application
{
private void Application_Startup(object sender, StartupEventArgs e)
{
MainWindow window = new MainWindow();
if (e.Args.Length != 1)
{
MessageBox.Show("Wrong number of arguments!", "An error has occured", MessageBoxButton.OK, MessageBoxImage.Error);
Environment.Exit(1);
}
window.Show();
}
}
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void ShowConnectionsButton_Click(object sender, RoutedEventArgs e)
{
LogsTextBox.Text += "text\n";
}
}
that are both initially generated by Visual Studio. I assume that MainWindow.xaml is for handling GUI-related stuff and App.xaml is for application's logic. So, my (simple) question is, how should I start the server part? Should it be
server = new Server();
server.start();
window.Show();
or maybe
window.Show();
new Thread(() =>
{
Thread.CurrentThread.IsBackground = true;
server = new Server();
server.start();
}).Start();
or maybe use BackgroundWorker?
Where you put the server logic will depend on how you want your UI to behave.
Usually, you will want the window to load separately with loading/retrieving data.
You could put your server call in the Loaded event. For example:
public void OnLoad(object sender, RoutedEventArgs e)
{
server = new Server();
server.start();
...
}
This will be called when the window has loaded, and can be started. How you update data bindings will depend on how your server object is built.

Change parent thread of a form

I am trying to find a way to change the parent of a thread of a winform back to the GUI thread. Right now i have to create the form from another thread but accessing become impossible from the main form that has the reference to it.
here a sample of what i have
public partial class MainForm : Form
{
// local instance of the sub form
private ViewForm SubForm { get; set;} = null;
public MainForm()
{
InitializeComponent();
Task.Run(() =>
{
// set the sub form
SubForm = new ViewForm();
}
// call the rest of the initialization of main form
InitializeCustomControls();
}
private void OpenViewWindow_Click(object sender, EventArgs e)
{
// if the window is instanciated
if (SubForm != null)
{
SubForm.Show();
}
}
}
The ViewForm window is not a Form object. it's a custom third party window. It has a lot of controls and templates mixed with themes. The sole call to a new empty constructor can take up to 7 seconds hence why i need to create it on another thread while i continue loading my main window.
Right now i can call any method in the window except .Show() which always fail due to thread creation restriction. I would like to stay away from creating the thread as an endless running thread that will wait and read some object to will tell him when to show and hide the window.
the is the .Show() error :
Cross-thread operation not valid: Control 'ViewForm' accessed from a thread other than the thread it was created on.
I did try the following instead but it still freeze my interface :
Task.Run(() =>
{
// set the sub form
this.Invoke(new MethodInvoker(delegate
{
SubForm = new ViewForm();
}));
}
What i would like is something like a fire and forget instantiation of a GUI object.
HereĀ“s a solution using a BackGroundWorker:
public partial class MainForm : Form
{
// local instance of the sub form
private ViewForm SubForm { get; set;} = null;
public MainForm()
{
InitializeComponent();
backGroundWorker1.RunWorkerAsync();
InitializeCustomControls();
}
private void OpenViewWindow_Click(object sender, EventArgs e)
{
// if the window is instanciated
if (SubForm != null)
{
SubForm.Show();
}
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
SubForm = new ViewForm();
}
}
Tested creating two forms, the MainForm pretty much like the one in your sample and the SubView form with a Threading.Sleep(10000) on the constructor.

Open a second winform asynchronously but still behave as a child to the parent form?

I am creating an application and I would like to implement a progress window that appears when a lengthy process is taking place.
I've created a standard windows form project to which I've created my app using the default form. I've also created a new form to use as a progress window.
The problem arises when i open the progress window (in a function) using:
ProgressWindow.ShowDialog();
When this command is encountered, the focus is on the progress window and I assume it's now the window who's mainloop is being processed for events. The downside is it blocks the execution of my lengthy operation in the main form.
If I open the progress window using:
ProgressWindow.Show();
Then the window opens correctly and now doesn't block the execution of the main form but it doesn't act as a child (modal) window should, i.e. allows the main form to be selected, is not centered on the parent, etc..
Any ideas how I can open a new window but continue processing in the main form?
You probably start your lengthy operation in a separate worker thread (e.g. using a background worker). Then show your form using ShowDialog() and on completion of the thread close the dialog you are showing.
Here is a sample - in this I assume that you have two forms (Form1 and Form2). On Form1 I pulled a BackgroundWorker from the Toolbox. Then I connected the RunWorkerComplete event of the BackgroundWorker to an event handler in my form. Here is the code that handles the events and shows the dialog:
public partial class Form1 : Form
{
public Form1() {
InitializeComponent();
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) {
Thread.Sleep(5000);
e.Result = e.Argument;
}
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
var dlg = e.Result as Form2;
if (dlg != null) {
dlg.Close();
}
}
private void button1_Click(object sender, EventArgs e) {
var dlg = new Form2();
this.backgroundWorker1.RunWorkerAsync(dlg);
dlg.ShowDialog();
}
}
I implemented something very similar to this for another project. This form allows you to popup a modal dialog from within a worker thread:
public partial class NotificationForm : Form
{
public static SynchronizationContext SyncContext { get; set; }
public string Message
{
get { return lblNotification.Text; }
set { lblNotification.Text = value; }
}
public bool CloseOnClick { get; set; }
public NotificationForm()
{
InitializeComponent();
}
public static NotificationForm AsyncShowDialog(string message, bool closeOnClick)
{
if (SyncContext == null)
throw new ArgumentNullException("SyncContext",
"NotificationForm requires a SyncContext in order to execute AsyncShowDialog");
NotificationForm form = null;
//Create the form synchronously on the SyncContext thread
SyncContext.Send(s => form = CreateForm(message, closeOnClick), null);
//Call ShowDialog on the SyncContext thread and return immediately to calling thread
SyncContext.Post(s => form.ShowDialog(), null);
return form;
}
public static void ShowDialog(string message)
{
//Perform a blocking ShowDialog call in the calling thread
var form = CreateForm(message, true);
form.ShowDialog();
}
private static NotificationForm CreateForm(string message, bool closeOnClick)
{
NotificationForm form = new NotificationForm();
form.Message = message;
form.CloseOnClick = closeOnClick;
return form;
}
public void AsyncClose()
{
SyncContext.Post(s => Close(), null);
}
private void NotificationForm_Load(object sender, EventArgs e)
{
}
private void lblNotification_Click(object sender, EventArgs e)
{
if (CloseOnClick)
Close();
}
}
To use, you'll need to set the SyncContext from somewhere in your GUI thread:
NotificationForm.SyncContext = SynchronizationContext.Current;
Another option:
Use ProgressWindow.Show() & implement the modal-window behavior yourself. parentForm.Enabled = false, position the form yourself, etc.

Categories

Resources