A Possible Threading/COM/UI problem - c#

I am writing a toolbar for IE(6+). I have used the various sample bars from
codeproject.com (http://www.codeproject.com/KB/dotnet/IE_toolbar.aspx), and have a toolbar that works, registers unregisters etc. What I want the toolbar to do is to highlight divs within an html page as the users' mouse moves over that div. So far the highlighting code works, but I would like to display the name of the div (if it exists) in a label on the toolbar (changing as the mouse moves etc).
I cannot for the life of me get this to happen and trying to debug it is a nightmare. As the assembly is hosted in IE, I suspect that I am causing an exception (in IE) by trying to update the text on the label from a thread that didn't create that control, but because that exception is happening in IE, I don't see it.
Is the solution to try to update the control in a thread-safe way using Invoke? If so how?
Here is the event code:
private void Explorer_MouseOverEvent(mshtml.IHTMLEventObj e)
{
mshtml.IHTMLDocument2 doc = this.Explorer.Document as IHTMLDocument2;
element = doc.elementFromPoint(e.clientX, e.clientY);
if (element.tagName.Equals("DIV", StringComparison.InvariantCultureIgnoreCase))
{
element.style.border = "thin solid blue;";
if (element.className != null)
{
UpdateToolstrip(element.className);
}
}
e.returnValue = false;
}
and here is an attempt at thread-safe update of the toolbar:
delegate void UpdateToolstripDelegate(string text);
public void UpdateToolstrip(string text)
{
if (this.toolStripLabel1.InvokeRequired == false)
{
this.toolStripLabel1.Text = text;
}
else
{
this.Invoke(new UpdateToolstripDelegate(UpdateToolstrip), new object[] { text });
}
}
Any suggestions much appreciated.

I can't really reproduce the issue (creating a test project for an IE toolbar is a tad too much work), but you can try this:
Add the following routine to a public static (extensions methods) class:
public static void Invoke(this Control control, MethodInvoker methodInvoker)
{
if (control.InvokeRequired)
control.Invoke(methodInvoker);
else
methodInvoker();
}
And then replace the section of similar code in the first block with this:
if (element.className != null)
{
this.Invoke(() => toolStripLabel1.Text = element.className);
}
This is a sure-fire way of avoiding thread-safe issues in UI applications.

Related

How to have C# Webbrowser handle webpage login popup for webscraping

I'm trying to programmatically login to a site like espn.com. The way the site is setup is once I click on the Log In button located on the homepage, a Log In popup window is displayed in the middle of the screen with the background slightly tinted. My goal is to programmatically obtain that popup box, supply the username and password, and submit it -- hoping that a cookie is returned to me to use as authentication. However, because Javascript is used to display the form, I don't necessarily have easy access to the form's input tags via the main page's HTML.
I've tried researching various solutions such as HttpClient and HttpWebRequest, however it appears that a Webbrowser is best since the login form is displayed using Javascript. Since I don't necessarily have easy access to the form's input tags, a Webbrowser seems the best alternative to capturing the popup's input elements.
class ESPNLoginViewModel
{
private string Url;
private WebBrowser webBrowser1 = new WebBrowser();
private SHDocVw.WebBrowser_V1 Web_V1;
public ESPNLoginViewModel()
{
Initialize();
}
private void Initialize()
{
Url = "http://www.espn.com/";
Login();
}
private void Login()
{
webBrowser1.Navigate(Url);
webBrowser1.DocumentCompleted +=
new WebBrowserDocumentCompletedEventHandler(webpage_DocumentCompleted);
Web_V1 = (SHDocVw.WebBrowser_V1)this.webBrowser1.ActiveXInstance;
Web_V1.NewWindow += new SHDocVw.DWebBrowserEvents_NewWindowEventHandler(Web_V1_NewWindow);
}
//This never gets executed
private void Web_V1_NewWindow(string URL, int Flags, string TargetFrameName, ref object PostData, string Headers, ref bool Processed)
{
//I'll start determing how to code this once I'm able to get this invoked
}
private void webpage_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
{
HtmlElement loginButton = webBrowser1.Document.GetElementsByTagName("button")[5];
loginButton.InvokeMember("click");
//I've also tried the below InvokeScript method to see if executing the javascript that
//is called when the Log In button is clicked, however Web_V1_NewWindow still wasn't called.
//webBrowser1.Document.InvokeScript("buildOverlay");
}
}
I'm expecting the Web_V1_NewWindow handler to be invoked when the InvokeMember("click") method is called. However, code execution only runs through the webpage_DocumentCompleted handler without any calls to Web_V1_NewWindow. It might be that I need to use a different method than InvokeMember("click") to invoke the Log In button's click event handler. Or I might need to try something completely different altogether. I'm not 100% sure the Web_V1.NewWindow is the correct approach for my needs, but I've seen NewWindow used often when dealing with popups so I figured I should give it a try.
Any help would be greatly appreciated as I've spent a significant amount of time on this.
I know it is the late answer. But it will help someone else.
You can extract the value from FRAME element by following
// Get frame using frame ID
HtmlWindow frameWindow = (from HtmlWindow win
in WbBrowser.Document.Window.Frames select win)
.Where(x => string.Compare(x.WindowFrameElement.Id, "frm1") == 0)
.FirstOrDefault();
// Get first frame textbox with ID
HtmlElement txtElement = (from HtmlElement element
in frameWindow.Document.GetElementsByTagName("input")
select element)
.Where(x => string.Compare(x.Id, "txt") == 0).FirstOrDefault();
// Check txtElement is nul or not
if(txtElement != null)
{
Label1.Text = txtElement.GetAttribute("value");
}
For more details check
this article

Find out which winforms controls are accessed from a background thread

We have built a huge winforms project, already in progress for multiple years.
Sometimes, our users get an exception which looks like this one.
The resolution of this problem seems to be:
don't acces UI components from a background thread
.
But since our project is a very big project with a lot of different threads, we don't succeed in finding all these.
Is there a way to check (with some tool or debugging option) which components are called from a background thread?
To clarify:
I created a sample winforms project with a single Form, containing two Button
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
button1.Text = "Clicked!";
}
private void button2_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
button2.BackColor = Color.Red; //this does not throw an exception
//button2.Text = "Clicked"; //this throws an exception when uncommented
});
}
}
The background color of button2 is set to red when the button is clicked. This happens in a background thread (which is considered bad behavior). However, it doesn't (immediately) throw an exception. I would like a way to detect this as 'bad behavior'. Preferably by scanning my code, but if it's only possible by debugging, (so pausing as soon as a UI component is accessed from a background thread) it's also fine.
I've got 2 recommendations to use together, the first is a Visual Studio Plugin called DebugSingleThread.
You can freeze all the threads and work on one at a time (obviously the non-main-UI threads) and see each threads access to controls. Tedious I know but not so bad with the second method.
The second method is to get the steps in order to reproduce the problem. If you know the steps to reproduce it, it will be easier to see whats causing it. To do this I made this User Action Log project on Github.
It will record every action a user makes, you can read about it here on SO: User Activity Logging, Telemetry (and Variables in Global Exception Handlers).
I'd recommend you also log the Thread ID, then when you have been able to reproduce the problem, go to the end of the log and work out the exact steps. Its not as painful as it seems and its great for getting application telemetry.
You might be able to customise this project, eg trap a DataSource_Completed event or add a dummy DataSource property that sets the real Grids DataSource property and raises an INotifyPropertyChanged event - and if its a non-main thread ID then Debugger.Break();.
My gut feeling is you're changing a control's (eg a grid) data source in a background thread (for that non-freeze feel) and thats causing a problem with synchronisation. This is what happened to the other DevExpress customer who experienced this. Its discussed here in a different thread to the one you referenced.
Is your app set to ignore cross threading intentionally?
Cross-thread operations should be blowing up all the time in winforms. It checks for them like crazy in just about every method. for a starting point check out https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/Control.cs.
Somewhere in your app, somebody might have put this line of code:
Control.CheckForIllegalCrossThreadCalls = False;
Comment that out and run the app, then follow the exceptions.
(Usually you can fix the problem by wrapping the update in an invoke, e.g., in a worker thread if you see textbox1.text=SomeString; change it to `textbox.invoke(()=>{textbox1.text=SomeString;});.
You may also have to add checking for InvokeRequired, use BeginInvoke to avoid deadlocks, and return values from invoke, those are all separate topics.
this is assuming even a moderate refactor is out of the question which for even a medium sized enterprise app is almost always the case.
Note: it's not possible to guarantee successful discovery of this case thru static analysis (that is, without running the app). unless you can solve the halting problem ... https://cs.stackexchange.com/questions/63403/is-the-halting-problem-decidable-for-pure-programs-on-an-ideal-computer etc...
I did this to search for that specific situation but of course, need to adjust it to your needs, but the purpose of this is to give you at least a possibility.
I called this method SearchForThreads but since it's just an example, you can call it whatever you want.
The main idea here is perhaps adding this Method call to a base class and call it on the constructor, makes it somewhat more flexible.
Then use reflection to invoke this method on all classes deriving from this base, and throw an exception or something if it finds this situation in any class.
There's one pre req, that is the usage of Framework 4.5.
This version of the framework added the CompilerServices attribute that gives us details about the Method's caller.
The documentation for this is here
With it we can open up the source file and dig into it.
What i did was just search for the situation you specified in your question, using rudimentary text search.
But it can give you an insight about how to do this on your solution, since i know very little about your solution, i can only work with the code you put on your post.
public static void SearchForThreads(
[System.Runtime.CompilerServices.CallerMemberName] string memberName = "",
[System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "",
[System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
var startKey = "this.Controls.Add(";
var endKey = ")";
List<string> components = new List<string>();
var designerPath = sourceFilePath.Replace(".cs", ".Designer.cs");
if (File.Exists(designerPath))
{
var designerText = File.ReadAllText(designerPath);
var initSearchPos = designerText.IndexOf(startKey) + startKey.Length;
do
{
var endSearchPos = designerText.IndexOf(endKey, initSearchPos);
var componentName = designerText.Substring(initSearchPos, (endSearchPos - initSearchPos));
componentName = componentName.Replace("this.", "");
if (!components.Contains(componentName))
components.Add(componentName);
} while ((initSearchPos = designerText.IndexOf(startKey, initSearchPos) + startKey.Length) > startKey.Length);
}
if (components.Any())
{
var classText = File.ReadAllText(sourceFilePath);
var ThreadPos = classText.IndexOf("Task.Run");
if (ThreadPos > -1)
{
do
{
var endThreadPos = classText.IndexOf("}", ThreadPos);
if (endThreadPos > -1)
{
foreach (var component in components)
{
var search = classText.IndexOf(component, ThreadPos);
if (search > -1 && search < endThreadPos)
{
Console.WriteLine($"Found a call to UI thread component at pos: {search}");
}
}
}
}
while ((ThreadPos = classText.IndexOf("Task.Run", ++ThreadPos)) < classText.Length && ThreadPos > 0);
}
}
}
I hope it helps you out.
You can get the Line number if you split the text so you can output it, but i didn't want to go through the trouble, since i don't know what would work for you.
string[] lines = classText.Replace("\r","").Split('\n');
Try that:
public static void Main(string[] args)
{
// Add the event handler for handling UI thread exceptions to the event.
Application.ThreadException += new ThreadExceptionEventHandler(exception handler);
// Set the unhandled exception mode to force all Windows Forms errors to go through the handler.
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
// Add the event handler for handling non-UI thread exceptions to the event.
AppDomain.CurrentDomain.UnhandledException += // add the handler here
// Runs the application.
Application.Run(new ......);
}
Then you can log the message and the call stack and that should give you enough information to fix the issue.
I recommend you update your GUI to handle this situation automatically for your convenience. You instead use a set of inherited controls.
The general principle here is to override the property Set methods in a way to make them Thread Safe. So, in each overridden property, instead of a straight update of the base control, there's a check to see if an invoke is required (meaning we're on a separate thread the the GUI). Then, the Invoke call updates the property on the GUI thread, instead of the secondary thread.
So, if the inherited controls are used, the form code that is trying to update GUI elements from a secondary thread can be left as is.
Here is the textbox and button ones. You would add more of them as needed and add other properties as needed. Rather than putting code on individual forms.
You don't need to go into the designer, you can instead do a find/replace on the designer files only. For example, in ALL designer.cs files, you would replace System.Windows.Forms.TextBox with ThreadSafeControls.TextBoxBackgroundThread and System.Windows.Forms.Button with ThreadSafeControls.ButtonBackgroundThread.
Other controls can be created with the same principle, based on which control types & properties are being updated from the background thread.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace ThreadSafeControls
{
class TextBoxBackgroundThread : System.Windows.Forms.TextBox
{
public override string Text
{
get
{
return base.Text;
}
set
{
if (this.InvokeRequired)
this.Invoke((MethodInvoker)delegate { base.Text = value; });
else
base.Text = value;
}
}
public override System.Drawing.Color ForeColor
{
get
{
return base.ForeColor;
}
set
{
if (this.InvokeRequired)
this.Invoke((MethodInvoker)delegate { base.ForeColor = value; });
else
base.ForeColor = value;
}
}
public override System.Drawing.Color BackColor
{
get
{
return base.BackColor;
}
set
{
if (this.InvokeRequired)
this.Invoke((MethodInvoker)delegate { base.BackColor = value; });
else
base.BackColor = value;
}
}
}
class ButtonBackgroundThread : System.Windows.Forms.Button
{
public override string Text
{
get
{
return base.Text;
}
set
{
if (this.InvokeRequired)
this.Invoke((MethodInvoker)delegate { base.Text = value; });
else
base.Text = value;
}
}
public override System.Drawing.Color ForeColor
{
get
{
return base.ForeColor;
}
set
{
if (this.InvokeRequired)
this.Invoke((MethodInvoker)delegate { base.ForeColor = value; });
else
base.ForeColor = value;
}
}
public override System.Drawing.Color BackColor
{
get
{
return base.BackColor;
}
set
{
if (this.InvokeRequired)
this.Invoke((MethodInvoker)delegate { base.BackColor = value; });
else
base.BackColor = value;
}
}
}
}

Binding FocusAction Behavior on Button Click

I am not very experienced using Behaviors, but so far they have come in handy to be able to execute code from the ViewModel but still triggering the actions from the View.
In my current scenario, I have a TextBox that appears when a Button is clicked. I would like to then set the focus of that TextBox after the Button has been clicked.
Previously, I have been able to set the focus using an EventTriggerBehavior like so:
<core:EventTriggerBehavior>
<behaviors:FocusAction />
</core:EventTriggerBehavior>
However, this will only suffice if I want to set the focus of that control when the View is loaded. In this case, the TextBox isn't visible at that time, and actually, the focus goes to a different TextBox initially.
Is there a way to set the focus of a control from the ViewModel? This is a WinRT 8.1 application, but will be being ported to Windows 10 Universal in the future.
EDIT
The answer here looks like it would do what I am looking for, but when I try it I get an error:
Cannot resolve symbol 'UIPropertyMetadata'
From what I can tell, that class exists in the System.Windows namespace, but even with a using System.Windows; I still get the same error. I have also tried new System.Windows.UIPropertyMetadata(null, ElementToFocusPropertyChanged) as well, but that doesn't make a difference either. Is that class not available in WinRT?
I was able to get it to work by slightly modifying the answer linked to in my original question. For anyone looking to accomplish this in a WinRT application, here is the modified code:
public class EventFocusAttachment
{
public static Control GetElementToFocus(Button button)
{
return (Control)button.GetValue(ElementToFocusProperty);
}
public static void SetElementToFocus(Button button, Control value)
{
button.SetValue(ElementToFocusProperty, value);
}
public static readonly DependencyProperty ElementToFocusProperty =
DependencyProperty.RegisterAttached("ElementToFocus", typeof(Control),
typeof(EventFocusAttachment), new PropertyMetadata(null, ElementToFocusPropertyChanged));
public static void ElementToFocusPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var button = sender as Button;
if (button != null)
{
button.Click += async (s, args) =>
{
var control = GetElementToFocus(button);
if (control != null)
{
await Task.Delay(100);
control.Focus(FocusState.Programmatic);
}
};
}
}
}
I had to add a small delay because the TextBox that I am focusing on is not visible until after the button is pressed, so it wasn't working without the delay. Also, I had to change the UIPropertyMetadata to simply PropertyMetadata, and add async to the lambda expression to allow for the await Task.Delay(100);

WP7 Text-To-Speech Playing Whole Collection At Once

I working on a Windows Phone 7 app with Text-To-Speech capabilities. I'm using Text-To-Speech with Microsoft Translator Service and the following C# code...
// Text-To-Speech with Microsoft Translator Service (http://translatorservice.codeplex.com/)
private void TextToSpeech_Play(object sender, EventArgs e)
{
SpeechSynthesizer speech = new SpeechSynthesizer(CLIENT_ID, CLIENT_SECRET);
//string content = "This is a beautiful day!";
string language = "en";
//speech.SpeakAsync(content, language);
foreach (UIElement element in todayschapter.Children)
{
if (element is TextBlock)
{
string content = (element as TextBlock).Text;
speech.SpeakAsync(content, language);
}
}
}
In this instance, todayschapter is a StackPanel and its Children are TextBlocks. I'm wanting to simply play audio of each TextBlock, in succession. The problem is that it is play the audio of EVERY TextBlock at the same time.
I have a sneaking suspicion that the problem is SpeakAsync(), but I'm not sure. The documentation shows Speak(), but that isn't available (maybe a different version) in the Visual Studio methods helper dropdown (little thing that shows as you type - not sure what it's called).
Is there a way to make it wait for each play to finish before playing the next? Is foreach not the right choice, for this?
As always, if my code just looks stupid, please recommend better ways. I'm very much a beginner programmer.
Just use the Speak instead of the async call, since you want to have it one after another anyway.
The SpeakAsync call is indeed the problem. Unfortunately, since SpeakAsync doesn't return a Task, you can't just convert this to await SpeakAsync() (which would be the most straightforward conversion).
And, looking at the source code, it doesn't fire an event to tell when it's done. So let's add one (in SpeechSynthesizer.cs):
public event EventHandler<SpeechEventArgs> SpeakCompleted;
public void SpeakAsync(string text, string language)
{
this.GetSpeakStreamAsyncHelper(text, language, result =>
{
if (result.Error == null)
{
SoundEffect effect = SoundEffect.FromStream(result.Stream);
FrameworkDispatcher.Update();
effect.Play();
this.OnSpeakCompleted(new SpeechEventArgs(result.Error)); // added to call completion handler
}
else
{
this.OnSpeakFailed(new SpeechEventArgs(result.Error));
}
});
}
// new function
private void OnSpeakCompleted(SpeechEventArgs e)
{
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
if (SpeakCompleted != null)
SpeakCompleted(this, e);
});
}
Now, you'll need to handle the SpeakCompleted event and start speaking the next string.
Something like this (I haven't even compiled this, so be warned):
private Queue<string> utterances;
private SpeechSynthesizer speech;
private void TextToSpeech_Play(object sender, EventArgs e)
{
speech = new SpeechSynthesizer(CLIENT_ID, CLIENT_SECRET);
speech.SpeechCompleted += new EventHandler<SpeechEventArgs>(TextToSpeech_Completed);
foreach (UIElement element in todayschapter.Children)
{
if (element is TextBlock)
{
string content = (element as TextBlock).Text;
utterances.Enqueue(content);
}
}
TextToSpeech_Completed(null, null); // start speaking the first one
}
private void TextToSpeech_Completed(object sender, SpeechEventArgs e)
{
if (utterances.Any())
{
string contents = utterances.Dequeue();
speech.SpeakAsync(contents);
}
}

Does data binding work on invisible control?

This is a .net problem with winforms, not asp.net.
I have a windows form with several tabs. I set data bindings of all controls when the form is loaded. But I have noticed that the data bindings of controls on the second tab do not work. Those bindings work only when the form is loaded and when I select the second tab. This brings the suspicion to me: data bindings work only when bound controls become visible.
Anyone can tell me whether this is true or not? It is not hard to test this but I would like to know some confirmation.
Thanks
You are correct. A data-bound control are not updated until the control is made visible.
The only reference I can find for this at the moment is this MSDN thread.
Your issue has to do with the behavior of the TabControl. See Microsoft bug report. I posted a workaround for that problem which subclasses the TabControl and 'Iniatalizes' all the tab pages when the control is created or the handle is created. Below is the code for the workaround.
public partial class TabControl : System.Windows.Forms.TabControl
{
protected override void OnHandleCreated(EventArgs e_)
{
base.OnHandleCreated(e_);
foreach (System.Windows.Forms.TabPage tabPage in TabPages)
{
InitializeTabPage(tabPage, true, Created);
}
}
protected override void OnControlAdded(ControlEventArgs e_)
{
base.OnControlAdded(e_);
System.Windows.Forms.TabPage page = e_.Control as System.Windows.Forms.TabPage;
if ((page != null) && (page.Parent == this) && (IsHandleCreated || Created))
{
InitializeTabPage(page, IsHandleCreated, Created);
}
}
protected override void OnCreateControl()
{
base.OnCreateControl();
foreach (System.Windows.Forms.TabPage tabPage in TabPages)
{
InitializeTabPage(tabPage, IsHandleCreated, true);
}
}
//PRB: Exception thrown during Windows Forms data binding if bound control is on a tab page with uncreated handle
//FIX: Make sure all tab pages are created when the tabcontrol is created.
//https://connect.microsoft.com/VisualStudio/feedback/details/351177
private void InitializeTabPage(System.Windows.Forms.TabPage page_, bool createHandle_, bool createControl_)
{
if (!createControl_ && !createHandle_)
{
return;
}
if (createHandle_ && !page_.IsHandleCreated)
{
IntPtr handle = page_.Handle;
}
if (!page_.Created && createControl_)
{
return;
}
bool visible = page_.Visible;
if (!visible)
{
page_.Visible = true;
}
page_.CreateControl();
if (!visible)
{
page_.Visible = false;
}
}
}
We've encountered a similar problem. We're trying to write to 2 bound, invisible fields so that we can change the format that we write to our dataset. This works fine when the objects are visible, but stops working when the visible property was changed to false.
To get round it, I added the following code:
// Stop our screen flickering.
chSplitContainer.Panel2.SuspendLayout();
// Make the bound fields visible or the binding doesn't work.
tbxValueCr.Visible = true;
tbxValueDb.Visible = true;
// Update the fields here.
<DO STUFF>
// Restore settings to how they were, so you don't know we're here.
tbxValueCr.Visible = false;
tbxValueDb.Visible = false;
chSplitContainer.Panel2.ResumeLayout();
I've struggled with this myself and concluded that the only workaround, besides subclassing apparently (see hjb417's answer), was to make the other tab visible. Switching to the other tab and going back to the previous immediately before the form is visible doesn't work. If you do not want to have the second tab visible, I've used the following code as a workaround:
this.tabControl.SelectedTab = this.tabPageB;
this.tabPageB.BindingContextChanged += (object sender, EventArgs e) => {
this.tabContainerMain.SelectedTab = this.tabPageA;
};
Assuming tabPageA is the visible tab, and tabPageB is the invisible one you want to initialize. This switches to pageB, and switches back once the data binding is complete. This is invisible to the user in the Form.
Still an ugly hack, but at least this works. Off course, he code gets even uglier when you have multiple tabs.
Sorry for necromancing this thread, but it is easy to force the invisible controls' databinding/handles to be ready using this method:
https://social.msdn.microsoft.com/Forums/vstudio/en-US/190296c5-c3b1-4d67-a4a7-ad3cdc55da06/problem-with-binding-and-tabcontrol?forum=winforms
Simply, let's say if your controls are in tab page tpg_Second (or tabCtl.TabPages[1]), before you do anything with their data, call this first:
tpg_Second.Show()
This will not activate any of the tab pages, but viola, the databinding of the controls should work now.
This is not something I've come across directly. However, you might be experiencing a problem with the BindingContext. Without more details it's hard to say, but if I were you I'd set a breakpoint and make sure the controls are all bound in the same context.
Based on the answers, I made this method that works for me:
public partial class Form1: Form
{
private void Form1_Load(object sender, EventArgs e)
{
...
forceBindTabs(tabControl1);
}
private void forceBindTabs(TabControl ctl)
{
ctl.SuspendLayout();
foreach (TabPage tab in ctl.TabPages)
tab.Visible = true;
ctl.ResumeLayout();
}
}
In addition to solving the problem, the tabs are loaded at the beginning and are displayed faster when the user clicks on them.

Categories

Resources