C# WinForms TabControl Remove Tab and close child controls - c#

I have a parent container (form) that initializes a new TabPage with a user control inside of it (called from the Menu Strip). The Tab Control has a Context Menu that, when you right click and select "Close Selected Tab", I call a method that Removes that tab. However, it does not stop the code within that TabPage's user control from running. How can I clean this up so when I close the Tab (TabPages.Remove... etc) that it also closes the user control within that TabPage so that any background code stops executing?
The code below is what closes the tab:
The latter part just selects the next tab to the left after the tab is removed.
public static void closeCurrentTab(TabControl tc)
{
int curTabIndex = tc.SelectedIndex;
TabPage tp = tc.SelectedTab;
tc.TabPages.Remove(tp);
if(curTabIndex > 0)
{
tc.SelectedTab = tc.TabPages[(curTabIndex - 1)];
}
}

I found the issue. I found that the asynchronous processes within the child control were running astray even after the parent control (in this case being the TabPage) was disposed of.
A simple example of how I solved it is below.
Also, for more complex task chaining , I can introduce cancellation tolkens once the parent becomes null, thus cleaning up whatever needs to be once a user closes a tab hosting that control, but doesnt close the overall application.
using this.Parent and testing whether it is null or not whevenever id like to.
protected override void OnParentChanged(EventArgs e)
{
base.OnParentChanged(e);
if(this.Parent == null)
{
// Clean up
}
}
and \ or this
private async Task popUp()
{
do
{
MessageBox.Show("Im running!");
await Task.Delay(5000);
}
while (this.Parent != null);
}

Related

How to avoid multiple event fire when WPF page is opened again?

I have a frame and few pages in my WPF application.
My navigation is controlled by buttons. On each button I have click handler that creates new page with some parameters:
private void ButtonProductionAuto_OnClick(ref TechModbus, RoutedEventArgs e)
{
FrameMain.Content = new PageProductionAuto(someobject, this);
}
private void ButtonProductionManual_OnClick(ref TechModbus, RoutedEventArgs e)
{
FrameMain.Content = new PageProductionManual(someobject, this);
}
When I'm switching between pages - previous pages still exist in memory and they react on some custom events.
(edit)
This is my code related with events:
public PageProductionAuto(ref TechModbus modbus, MainWindow wnd)
{
// ...
wnd.KeyDown += Wnd_KeyDown;
wnd.KeyUp += Wnd_KeyUp;
m.OnReadFinished += Modbus_OnReadFinished;
// ...
}
How can I dispose these pages or how can I avoid double-fire on my events when page is opened second time?
You should unregister the events on leaving the page.
GarbageCollector will then "dispose" (it's actually not a dispose) by itsself when there are no more references on those objects(PageProductionAuto and PageProductionManual).
Quoting MS:
The reason WPF controls don't implement IDisposable is because they have nothing to dispose. They have no handle to clean up, and no unmanaged resources to release. To ensure your memory is cleaned up, just make sure nothing has a reference to the controls once you're finished with them.
Your question is incomplete. But I can answer the question about "how to avoid multiple instances" part of it. To dispose your pages, you have to detach your events, remove them from the "openedPages" collection, and dispose where possible.
List<object> openedPages = new List<object>();
private void ButtonProductionAuto_OnClick(object sender, RoutedEventArgs e)
{
var page = openedPages.FirstOrDefault(p => p.GetType().Equals(typeof(PageProductionAuto)));
if(page == null)
{
page = new PageProductionAuto(someobject, this);
opendPages.Add(page);
}
else
{
page.SetObjects(someobject, this); // create a method to set "someObject" to your page.
}
FrameMain.Content = page;
}
What I did to avoid this was I have a window with 2 frames in it. I placed one of each source XAML in each frame so frame 1 was XAML 1 and frame 2 was XAML 2. Then I just edited the visibility to collapsed and visible. Then you don't have any page changes or instances getting created. You just create the original 2 instances.

How to close all open instances of a modeless dialog box?

I have a C# winform project that displays a list of results based on a user's search criteria. For each item on the list, the user can open a modeless dialog box showing more details about the selected item.
Every time the user opens an instance of my details window, this code runs:
public void showDetails()
{
GetDetails route = new GetDetails();
route.myParent = this;
route.Show();
}
In order to compare details between two or more items, the user is allowed to open as many instances of this dialog box as it likes. I'd like to be able to close any and all open instances of this window when the user conducts a new search from the main form window? I've tried Googling, but no luck ... does anyone know how to do this?
Application.OpenForms is a collection of open forms owned by the application
try find all details dialogs and close them like this:
foreach(var f in Application.OpenForms.OfType<GetDetails>().ToList())
{
f.Close();
}
You don't really tell, but I assume your GetDetails is a System.Windows.Forms.Control (probably a form, a dialog box, a message box, etc).
If you look closely to your Form.InitializeComponent, you'll see that Form has a property Controls. All child controls are added to the control collection.
If you add each created route to your control collection you can ask this collection for all objects of type GetDetails and order them to close:
public void ShowDetails()
{
var route = new GetDetails();
route.myParent = this;
this.Controls.Add(route);
route.Show();
}
public void CloseAllRoutes()
{
foreach (var route in this.Controls.Where( control => control is GetDetails))
{
route.Close();
}
}
You need to be certain that when a rout is closed, or disposed or something the following code is called:
private void OnRouteClosed (object sender, ...)
{
if (sender is GetDetails)
{
this.Controls.Remove(sender);
}
}

C# WPF - Want to open multiple windows at once, but only one instance of each window

I am new to WPF and have been hunting for an answer, surely this is not difficult?
I have created a main window with links to multiple windows, and I want them to run modelessly alongside one another, but I don't want to open multiple instances of the SAME window.
In simple terms, I can have Windows A, B, C open at once, but not Windows, A, A, B, C, C.
I need to implement a check for the window I'm trying to open (in this case, EditSettings).
If open - activate it
if not open, open it.
I have the following code in Main, which is not working.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void MenuItem_Click(object sender, RoutedEventArgs e)
{
EditSettings winEditSettings = new EditSettings();
string isOpen = null;
if (isOpen == "true")
{
winEditSettings.Activate();
}
else
{
winEditSettings.Show();
isOpen = "true";
}
}
}
Now I know what's wrong with this logic - every time I press the button to open EditSettings, it's setting isOpen to null again. If I don't set a value to isOpen, the If condition breaks.
I could initialise the variable 'isOpen' as a public variable outside the MenuItem_Click method, but then I think I would need an isOpen variable for each window I create!! Surely there is a better way?
The other option I tried is:
private void MenuItem_Click(object sender, RoutedEventArgs e)
{
EditSettings winEditSettings = new EditSettings();
if (winEditSettings.IsLoaded)
{
winEditSettings.Activate();
}
else { winEditSettings.Show(); }
I can't figure out why this isn't working, I tried isVisible, isLoaded, isActive - nothing is stopping the window from opening more than once. Thank you for the help!
There are people who'll perhaps throw a fit at the idea, but whenever I've needed to do this, I made the child window objects part of the application. Then, in your MenuItem_Click(), test if winEditSettings is null, instead.
It's still a member variable for each window (like your provisional isOpen solution), but having the window objects available can have advantages later, if you need to bridge information between the windows. In my cases, we wanted to be able to close all the child windows together, which (most trivially) meant keeping track of those objects in a central location.
Alternatively, if you want the setup completely decoupled, you could take a singleton-like approach and put the logic into your child window classes. Specifically, you could call EditSettings.Activate and let the class keep track of whether a window needs to be created or the existing window merely Show()n.
If I were handed your code to rewrite, I'd move it something like this:
private static EditSettings winEditSettings = null;
public static void WakeUp()
{
if (winEditSettings == null)
{
winEditSettings = new EditSettings();
}
winEditSettings.Activate(); // This may need to be inside the block above
winEditSettings.Show();
}
Both of those are part of the class (static), rather than an instance. Your application object therefore calls EditSettings.WakeUp() inside the original MenuItem_Click(), and never actually sees the child window, itself.
If you change your mind about the decoupled architecture later, by the way, you can add a get accessor to your winEditSettings and keep everybody fairly happy.
if (_adCst == null)
{
_adCst = new AddCustomerPage();
_adCst.WindowStartupLocation = System.Windows.WindowStartupLocation.CenterScreen;
_adCst.WindowState = System.Windows.WindowState.Normal;
_adCst.ResizeMode = System.Windows.ResizeMode.NoResize;
_adCst.Activate(); // This may need to be inside the block above
_adCst.Show();
}
else
{
if (!_adCst.IsLoaded == true)
{
_adCst = new AddCustomerPage();
_adCst.WindowStartupLocation = System.Windows.WindowStartupLocation.CenterScreen;
_adCst.WindowState = System.Windows.WindowState.Normal;
_adCst.ResizeMode = System.Windows.ResizeMode.NoResize;
_adCst.Show();
}
_adCst.Activate();
}
My suggestion would be that you set some form of a counter. This will prevent more than one instance of the window being opened.
int windowOpen = 1;
private void button_Click(object sender, RoutedEventArgs e)
{
if (windowOpen == 1)
{
WindowA winA = new WindowA();
winA.Show();
windowOpen++; //increments windowOpen by 1, windowOpen will now = 2
}
else if (windowOpen > 1)
{
MessageBox.Show("Window is already open");
}
}
I hope this helps.
For anyone else with this question, I have found another solution - which works except that it doesn't manage to bring the open window to the front (Activate). It does, however, prevent opening the same window more than once.
foreach (Window n in Application.Current.Windows)
if (n.Name == "winEditSettings")
{ winEditSettings.Activate(); }
else
{ winEditSettings.Show(); }
Can anyone speculate on why the window is not brought to the front, with Activate()?
EDIT
For others with this question, placing the winEditSettings.Activate() outside of the foreach loop does everything I'm trying to achieve:
foreach (Window n in Application.Current.Windows)
if (n.Name == "winEditSettings")
{ }
else
{ winEditSettings.Show(); }
winEditSettings.Activate();
This will stop multiple instances of the same window from opening, and will bring the window to the front if the user attempts to reopen it.

disabling mdi child maximizing when activating by clicking on title bar

I have an MDI form that I open multiple child windows into. I have noticed that if I click the title bar of an a child form that is not the active mdi child, that that child becomes actived and maximizes. This behavior does NOT happen when I click the unactivated child in a place other than the title bar. How can I disable the maximizing in this case? I still want to be able to maximize the form using the maximize button on the title bar. It would also be nice to be able to maximize a form when double-clicking the title bar (just not single-clicking).
I have found this routine to be causing the problem, but not sure how. This method is called whenever when an mdi child's activated event is called. This specific statement is causing the problem: "if (dv.Disk.IsOS9)"
private void UpdateDiskMenu()
{
if (this.ActiveMdiChild == null)
{
diskToolStripMenuItem.Enabled = false;
}
else
{
diskToolStripMenuItem.Enabled = true;
DiskViewer dv = (DiskViewer)this.ActiveMdiChild;
if (dv.Disk.IsOS9) // <----- Problem occurs here.
{
//if (((OS9Format)dv.Disk).BootstrapLSN > 0)
// bootstrapToolStripMenuItem.Enabled = true;
//else
// bootstrapToolStripMenuItem.Enabled = false;
}
}
}

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