problem :globalization in C# using VS2005 - c#

I want to globalize my application.
I have created a small form which asks the user their language.
I have a number of problems :
Problem 1:
In program.cs
new SplashScreen(_tempAL);
new LangForm(_lang);
Application.Run(new Form1(_tempAL, _lang));
I want the application not to call Form1 until the user clicks on OK in LangForm .
For more explaintion in LangForm :
public LangForm(char _langChar)
{
InitializeComponent();
_ch = _langChar;
this.TopMost = true;
this.Show();
}
private void _btnOk_Click(object sender, EventArgs e)
{
string _langStr = _cbLang.SelectedText;
switch (_langStr)
{
case "English":
_ch = 'E';
this.Hide();
break;
case "Arabic":
_ch = 'A';
this.Hide();
break;
case "Frensh":
_ch ='F';
this.Hide();
break;
}
_pressedOk = true;
}
private void _btnCancel_Click(object sender, EventArgs e)
{
this.Close();
Application.Exit();
}
Now when I debug, the application calls LangForm and then Form1 so both forms are shown.
I want Form1 to wait until the user clicks on Ok in LangForm.
Problem 2:
When should I check on the language? It's not allowed to check in "initializeComponent()"
so should I check after this function and then set controls location according to the language.
Problem 3:
Within application process I displays some message so before each "MessageBox.Show("");" I should check for the language or there is another way where I may set the language once.
Problem 4:
I have searched for interfaces for MessageBox as actually I want to change its layout. How can I find templates for MessageBox?
Thanks in-advance.

Display the language selection form as a dialog. Make your Program.cs file look like this:
[STAThread]
static void Main() {
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
if (DialogResult.OK == new LangForm().ShowDialog()) {
Application.Run(new Form1());
}
}
Add this line to your _btnOK click handler:
this.DialogResult = DialogResult.OK;

To block until a form has closed, use .ShowDialog() on the LangForm. I would then set the culture (Thread.CurrentThread.CurrentCulture and Thread.CurrentThread.CurrentUICulture) between this form closing and creating the new form. Having done this, anything from resx should load correctly.
For changing the layout of MsgBox (beyond the norm) you would have to write your own (it doesn't support this).
Something like:
[STAThread]
static void Main() {
Application.EnableVisualStyles();
// find the culture we want to use
bool cont;
string languageCode;
using (LangForm lang = new LangForm()) {
cont = lang.ShowDialog() == DialogResult.OK;
languageCode = lang.LanguageCode; // "en-US", etc
}
if (!cont) return;
// set the culture against the UI thread
Thread.CurrentThread.CurrentCulture =
Thread.CurrentThread.CurrentUICulture =
CultureInfo.GetCultureInfo(languageCode);
// show the main UI
using (MainForm main = new MainForm()) {
Application.Run(main);
}
}
Note the use of the official culture codes will make it easier to use things like CultureInfo; if you want to use your own short-list, then use an enum, and write a method somewhere:
public static string GetCultureCode(MyCulture culture) {
switch(culture) {
case MyCulture.French: return "fr-FR";
case MyCulture.English: return "en-GB";
//...
default: throw new NotSupportedException("Unexpected culture: " + culture);
}
}

Related

Display my forms in another class

I recently developped an application for a student project, and everything works fine. However, if I'm asking something here, you can certainly assume that the whole thing isn't so functional...and you'd be right hohoho. Let's get started. Basically the functional version of my project uses an UI console.
It runs, but from an user point of view, working with something like this isn't the most amazing stuff in the world. So I decided to replace my old console by some Windows Forms.
Project global render with console
Project global render with form
This is where things become wrong. I have "3" classes :
Program.cs (the main program with all the serious stuff)
formOne.cs (the first form with a button for each options)
form/Two to Five/.cs (each options open the corresponding form)
At some point, Program.cs will launch formOne.cs, and from there the user should be able to navigate between the various options and so the various forms...but nope. formOne.cs opens, and then we can't click on anything (well we can but nothing happens). I did a mistake somewhere, I would like to know where and how to fix it. Here's how I proceeded :
(this is the basic algorithm, not the whole code)
Program.cs :
class Program {
formOne winRecep = new formOne();
formTwo winCrea = new formTwo();
formThree winSearch = new formThree();
formFour winDel = new formFour();
formFive winView = new formFive();
winRecep.ShowDialog();
string userChoice = winRecep.getUserChoice();
switch(userChoice){
case "create new task" :
winCrea.ShowDialog();
break;
case "search a task" :
winSearch.ShowDialog();
break;
case "delete a task" :
winDel.ShowDialog();
break;
case "view my tasks" :
winView.ShowDialog();
break;
}
}
formOne.cs :
class formOne {
string userChoice;
public formOne()
{
InitializeComponent();
}
public string getUserChoice()
{
return userChoice;
}
private void formOne_Load(object sender, EventArgs e)
{
//blabla update current date, current hour...
}
private void buttonOptionOne_Click(object sender, EventArgs e)
{
userChoice = "create new task";
}
private void buttonOptionTwo_Click(object sender, EventArgs e)
{
userChoice = "search a task";
}
private void buttonOptionThree_Click(object sender, EventArgs e)
{
userChoice = "delete a task";
}
private void buttonOptionFour_Click(object sender, EventArgs e)
{
userChoice = "view my tasks";
}
}
It seems pretty clear to me, but I did a mistake somewhere. I would like to work in Program.cs instead of formOne.cs because, well, everything is in Program.cs so the most logical way to proceed is certainly to work here instead of bring informations and variables everywhere accross the classes.
Could someone help me and explain why it's wrong ?
EDIT : Program.cs using Application doesn't solve the thing :
class Program {
var winRecep = new formOne();
var createtask = new formTwo();
var viewTask = new formThree();
var searchTask = new formFour();
var deleteTask = new formFive();
Application.Run(winRecep);
string userChoice = winRecep.getUserChoice();
switch(userChoice){
case "create new task" :
Application.Run(createtask);
break;
case "search a task" :
Application.Run(searchTask);
break;
case "delete a task" :
Application.Run(deleteTask);
break;
case "view my tasks" :
Application.Run(viewTask);
break;
}
}
You need to start your main form like this:
var mainForm = new formOne();
Application.Run(mainForm);
This starts the windows message loop. Without a message loop, your application can't respond to any events (such as mouse clicks or keypresses).
The first thing you should do, is opening the main form using Application.Run, as already suggested. This starts the message loop which is essential for the rest of the execution:
var mainForm = new formOne();
Application.Run(mainForm);
Then use that form to open all others. Closing the form started with Application.Run will close the application since the message loop will end too. So on any action, like a button click or a command written, open the form you need. You can call Show to open the other form and make it possible to still access the main form, or ShowDialog which will block further actions until the child form was closed.

ManagementObjectSearcher causes re-entrancy issues for onclick handler

I am having an odd problem with protecting a section of code. My application is a tray app. I create a NotifyIcon inside my class (ApplicationContext). I have assigned a balloon click handler and a double click handler to the NotifyIcon object. there is also a context menu but I am not showing all code. Only important pieces.
public class SysTrayApplicationContext: ApplicationContext
{
private NotifyIcon notifyIcon;
private MainForm afDashBoardForm;
public SysTrayApplicationContext()
{
this.notifyIcon = new NotifyIcon();
this.notifyIcon.BalloonTipClicked += notifyIcon_BalloonTipClicked;
this.notifyIcon.MouseDoubleClick += notifyIcon_MouseDoubleClick;
// ... more code
}
Both handlers launch or create/show my form:
private void notifyIcon_MouseDoubleClick(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
openDashboard();
}
}
private void notifyIcon_BalloonTipClicked(object sender, EventArgs e)
{
openDashboard();
}
private void openDashboard()
{
if (dashBoardForm != null)
{
log.Debug("Dashboard form created already, so Activate it");
dashBoardForm.Activate();
}
else
{
log.Debug("Dashboard form does not exist, create it");
dashBoardForm = new MainForm();
dashBoardForm.Show();
}
}
There is a problem with the above code. Maybe more than 1. Issue: it is possible to display 2 dashboard forms which is not what I want. If user double clicks on tray icon while balloon message is displaying causes a race condition in openDashboard. I can reproduce this easily. So I added a lock around the code in openDashboard code and, to my surprise, that did NOT prevent 2 dashboard forms from displaying. I should not be able to create 2 MainForms. Where am I going wrong here?
here is the updated code with lock statement:
private void openDashboard()
{
lock (dashBoardFormlocker)
{
if (dashBoardForm != null)
{
log.Debug("Dashboard form created already, so Activate it");
dashBoardForm.Activate();
}
else
{
log.Debug("Dashboard form does not exist, create it");
dashBoardForm = new MainForm();
dashBoardForm.Show();
}
}
}
Note: lock object was added to the class and initialized in constructor.
private object dashBoardFormlocker;
UPDATE: Showing more code. this is how code gets started :
static void Main()
{
if (SingleInstance.Start())
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
XmlConfigurator.Configure();
// For a system tray application we don't want to create
// a form, we instead create a new ApplicationContext. The Run method takes
Application.Run(new SysTrayApplicationContext());
SingleInstance.Stop();
SingleInstance.Dispose();
}
}
}
UPDATE 2: Provide more code for clarity
public partial class MainForm : Form
{
public MainForm()
{
log.Trace("MainForm constructor...");
InitializeComponent();
// ... code not shown
this.label_OSVersion.Text = getOSFriendlyName();
// .. more code
}
private string getOSFriendlyName()
{
try
{
string result = string.Empty;
var mgmtObj = (from x in new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem").Get().OfType<ManagementObject>()
select x.GetPropertyValue("Caption")).FirstOrDefault();
result = mgmtObj != null ? mgmtObj.ToString() : string.Empty;
OperatingSystem os = Environment.OSVersion;
String sp = os.ServicePack ?? string.Empty;
return !string.IsNullOrWhiteSpace(result) ? result + sp : "Unknown";
}
catch (System.Exception ex)
{
log.Error("Error trying to get the OS version", ex);
return "Unknown";
}
}
}
The main UI thread must always pump a message loop to support communication from COM components.
So when you do a blocking operation from the UI thread like locking or joining a thread, (EDIT: edited based on Peter Duniho's fix) the UI thread will enter an 'alertable' state, allowing COM to dispatch certain type of messages, which in turn can cause re-entrancy issues like in your scenario.
Look at the answer to this question (Why did entering a lock on a UI thread trigger an OnPaint event?) for a much more accurate explanation.
Looking at the source code of ManagementObjectSearcher.Get there is a lock (inside Initialize), and since you call it from the constructor of your form, it may lead to the second event triggering while the form's constructor has not finished. The assignment to the dashBoardFormlocker variable only happens after the constructor finishes, so that would explain why it was null on the second entry.
The moral of the story is never do blocking operations on the UI thread.
Without a good, minimal, complete code example that reliably reproduces the problem, it's impossible to know for sure what the problem is. But the guess by answerer tzachs seems reasonable. If so, you can fix your problem by changing your method to look like this:
private bool _dashboardOpen;
private void openDashboard()
{
if (_dashboardOpen)
{
if (dashBoardForm != null)
{
log.Debug("Dashboard form created already, so Activate it");
dashBoardForm.Activate();
}
}
else
{
log.Debug("Dashboard form does not exist, create it");
_dashboardOpen = true;
dashBoardForm = new MainForm();
dashBoardForm.Show();
}
}
In that way, any re-entrant attempt to open the window will be detected. Note that you still need the check for null before actually activating; you can't activate a window that hasn't actually finished being created yet. The subsequent call to Show() will take care of activation anyway, so ignoring the activation in the re-entrant case shouldn't matter.

Minimized WinForms app restoring to Normal on second launch

First of all, I want to explain the "second launch": I use the SingleInstanceController approach to make it possible to call my app's EXE file, and accept arguments.
That way other apps, or users can tell the application to take a specific action.
The app is set to start with a WindowState of Minimized, and only if the user clicks the tray icon it restores to Normal.
But what I'm seeing is, that the first time I launch the application it stays minimized. Then when I call the EXE file for the second time, it restores to a normal window state.
I have no code that alters the window state.
I suspect this is because something else is triggering the restore.
The code of my SingleInstanceController looks like this:
public class SingleInstanceController : WindowsFormsApplicationBase
{
public SingleInstanceController()
{
IsSingleInstance = true;
StartupNextInstance += this_StartupNextInstance;
}
void this_StartupNextInstance(object sender, StartupNextInstanceEventArgs e)
{
Form1 form = MainForm as Form1;
string command = e.CommandLine[1];
switch (command.ToLowerInvariant())
{
case "makecall":
string phoneNumber = e.CommandLine[2];
PhoneAppHelper.MakePhoneCall(phoneNumber);
break;
default:
System.Windows.Forms.MessageBox.Show("Argument not supported");
break;
}
}
protected override void OnCreateMainForm()
{
MainForm = new Form1();
}
}
On my form, I have a listbox to show connected devices (USB), and a multiline textbox to show some activity, most for debug/information purposes.
Could the interaction with the controls on the form cause the restore?
Yes, this is the default behavior for WindowsFormsApplicationBase.OnStartupNextInstance(). You can simply fix that by overriding the method instead of using the event. Do note that you probably still want this to happen when you have a message to display. So make it look similar to this:
protected override void OnStartupNextInstance(StartupNextInstanceEventArgs e) {
//...
switch (command.ToLowerInvariant()) {
// etc..
default:
base.OnStartupNextInstance(e); // Brings it to the front
System.Windows.Forms.MessageBox.Show("Argument not supported");
break;
}
}

Proper way to change language at runtime

What is the proper way to change Form language at runtime?
Setting all controls manually using recursion like this
Save language choice to file > Restart Application > Load languge
choice before InitializeComponent();
Using Form constructor to replace instance of active from (if this is even possible)
Something else
There is so much half written threads about this but none provides real answer on what is proper way to do this?
UPDATE:
To clarify my question:
Doing something like this:
public Form1()
{
Thread.CurrentThread.CurrentUICulture = new CultureInfo("de");
this.InitializeComponent();
}
works fine and all my controls and everything else in resources get translated correctly.
And doing something like:
private void button1_Click(object sender, EventArgs e)
{
Thread.CurrentThread.CurrentUICulture = new CultureInfo("en");
}
does nothing, Form stays in language I set up before InitializeComponent();
I believe the solution shown in Hans Passant's comment might be the only (general) solution.
Personally, I use this base class for all forms that need to be localized:
public class LocalizedForm : Form
{
/// <summary>
/// Occurs when current UI culture is changed
/// </summary>
[Browsable(true)]
[Description("Occurs when current UI culture is changed")]
[EditorBrowsable(EditorBrowsableState.Advanced)]
[Category("Property Changed")]
public event EventHandler CultureChanged;
protected CultureInfo culture;
protected ComponentResourceManager resManager;
/// <summary>
/// Current culture of this form
/// </summary>
[Browsable(false)]
[Description("Current culture of this form")]
[EditorBrowsable(EditorBrowsableState.Never)]
public CultureInfo Culture
{
get { return this.culture; }
set
{
if (this.culture != value)
{
this.ApplyResources(this, value);
this.culture = value;
this.OnCultureChanged();
}
}
}
public LocalizedForm()
{
this.resManager = new ComponentResourceManager(this.GetType());
this.culture = CultureInfo.CurrentUICulture;
}
private void ApplyResources(Control parent, CultureInfo culture)
{
this.resManager.ApplyResources(parent, parent.Name, culture);
foreach (Control ctl in parent.Controls)
{
this.ApplyResources(ctl, culture);
}
}
protected void OnCultureChanged()
{
var temp = this.CultureChanged;
if (temp != null)
temp(this, EventArgs.Empty);
}
}
Then instead of directly changing Thread.CurrentThread.CurrentUICulture, I use this property in static manager class to change UI culture:
public static CultureInfo GlobalUICulture
{
get { return Thread.CurrentThread.CurrentUICulture; }
set
{
if (GlobalUICulture.Equals(value) == false)
{
foreach (var form in Application.OpenForms.OfType<LocalizedForm>())
{
form.Culture = value;
}
Thread.CurrentThread.CurrentUICulture = value;
}
}
}
I have found another way:
Move initialization form code in a private method like below:
private void FormInitialize() {/*Your code here*/}
In the form constructor use it like this:
public Form1()
{
InitializeComponent();
FormInitialize();
}
And from Button, menuItem or other call method like this:
private void ChangeCultureToFrench_Click(object sender, EventArgs e)
{
Thread.CurrentThread.CurrentUICulture = new CultureInfo("fr");
this.Controls.Clear();
this.InitializeComponent();
FormInitialize();
}
I hope this helps ;-)
I've discovered this kind of approach a few minutes ago. Just quick and simple restart of the main form. Meybe it will help to someone. Event is created inside the form on my own, called when user selects the language from menu but after the selected culture's name is saved into the settings. Culture names are then loaded from that settings. Works exactly as I need and looks like proper solution.
static class Program
{
private static bool doNotExit = true;
private static FormMain fm;
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
while(doNotExit)
{
System.Threading.Thread.CurrentThread.CurrentCulture = new CultureInfo(Properties.Settings.Default.language);//
System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(Properties.Settings.Default.language);//
doNotExit = false;
fm = new FormMain();
fm.lanugageChangedEvent += new EventHandler(main_LanugageChangedEvent);
Application.Run(fm);
}
}
static void main_LanugageChangedEvent(object sender, EventArgs e)
{
doNotExit = true;
fm.Close();
}
}
In reference to your ColumnHeader .NET framework bug, I also discovered this bug recently and posted a question about it (to which I haven't received any responses). I was able to fix the problem by hardcoding the changes for the ColumnHeaders. For example:
resources.ApplyResources(_myHeader, "_myHeader", culture);
You basically just replace the call to .Name with a literal string of the name. I have tested this and it works. Unfortunately this means it won't fit as part of the code you use to change all of the controls. You will have to add a line for each ColumnHeader object you need to change. If you have a listview with a variable number of columns, that could get tricky. Another option is to create localized resource files. I assume you probably already have them for stuff like message box text and other strings. Then you can add an entry in your resource file like "columnHeader_myHeader" and set the appropriate language text for each language. Finally, you can manually change the text to your column headers by using:
_myHeader.Text = myResourceFileName.columnHeader_myHeader;
This will select the appropriate language based on the current thread culture.
Hans was correct in that there doesn't seem to be a foolproof "proper" way to perform localization in .NET, though there are a variety of tools you can use. For something like a job application, even though it is probably already too late for this advice, my suggestion would be to learn as many different methods as you can for localization, learn the pros and cons, and then just pick a system and be able to argue why you believe it is the "proper" choice. They are probably more concerned with your logic and reasoning and demonstration of prior experience than they are with the actual method.
Hope this would help anyone, I found it best for me cause i needed to change buttons location according the lang (browse on the right or left of the search box and labels next to text fields).
save a public var on the main that will hold the lang.
created a class which handles the visual part
created xml files that will hold any language data and more (in my xml tag name=object name).
sent that class's constructor the form (to save and work with)
connect to that current xml file
From main form call whenever you want to initialView (part of the view class) and change lang (and more) anytime (just connect to the right xml file):
public void initialView()
{
//Set rightToLeft values
initialIndent(mainForm);
//set visual text values
initialTxt();
}
private void initialTxt()
{
// Are there any more controls under mainObj (Form1)?
Boolean endOfElemsUnderPnl = false;
// The current Control im working on
Object curObj = mainForm;
do
{
// MenuStrip needs to be handled separately
if (typeof(MenuStrip).ToString().Equals(curObj.GetType().ToString()))
{
foreach (ToolStripMenuItem miBase in ((MenuStrip)(curObj)).Items)
{
miBase.Text = mainForm.dbCon.getData(miBase.Name.ToString());
foreach (ToolStripMenuItem miInnerNode in miBase.DropDownItems)
{
miInnerNode.Text = mainForm.dbCon.getData(miInnerNode.Name.ToString());
}
}
}
// Any other Control i have on the form
else
{
((Control)(curObj)).Text = mainForm.dbCon.getData(((Control)(curObj)).Name.ToString());
}
curObj = mainForm.GetNextControl(((Control)(curObj)), true);
// Are there any more controls under mainObj?
if (null == curObj)
{
endOfElemsUnderPnl = true;
}
} while (!endOfElemsUnderPnl);
}
private void initialIndent(frmMyFileManager parent)
{
if (parent.Language.Equals("Hebrew"))
{
parent.RightToLeft = RightToLeft.Yes;
}
else if (parent.Language.Equals("English"))
{
parent.RightToLeft = RightToLeft.No;
}
else
{
parent.RightToLeft = RightToLeft.No;
}
}
And this is an example of how easy it is for my at runtime:
private void selectLanguageToolStripMenuItem_Click(object sender, EventArgs e)
{
DialogResult res = MessageBox.Show(this, "click yes for english and no for hebrew", "Select language", MessageBoxButtons.YesNoCancel);
if (DialogResult.Yes == res)
{
Language = "English";
}
else if (DialogResult.No == res)
{
Language = "Hebrew";
}
dbCon = new CDBConnector("****\\lang" + Language + ".xml");
view.initialView();
}

How to handle several windows form

I'm doing a small project to store movies. I'm using three windows form, a intro form, a main form and a new movie form. I also have a class that I call MovieManager that is the heart of the application. My problem is that I'm not sure how I should handle this windows.
Let's say I want the application to start with the intro form and when the user click on the OK-button the main form should appear. What is the best way to do this? Should I in the Program.cs create an object of the MovieManager class that show and hide the different windows form or should I in the Program.cs just start by showing the intro form?
You can simply do all staff in Program.cs when starting application. Show your IntroForm as dialog. If user clicks OK, then start main application form, otherwise close application.
static void Main()
{
IntroForm introForm = new IntroForm();
if (introForm.ShowDialog() != DialogResult.OK)
{
Application.Exit();
return;
}
Application.Run(new MainForm());
}
If you need single MovieManager instance for all these forms, then you can create it in Main method and pass same instance to IntroForm and MainForm:
MovieManager movieManager = new MovieManager();
IntroForm introForm = new IntroForm(movieManager);
if (introForm.ShowDialog() != DialogResult.OK)
{
Application.Exit();
return;
}
Application.Run(new MainForm(movieManager));
Also you can implement MovieManager as Singleton, which will be accessible everywhere via static property MovieManager.Instance.
What you call intro form, I should call a splash screen. In the program.cs I should just pop-up the splash screen (with a logo, title and info about the application, version number, etc.). The splash screen is shown for a certain amount of time (use a timer for this, or Thread.Sleep is alos possible altough a bit heavy).
When the splash screen closes show the MainForm is shown a from there you can instantiate a MovieManager or use a static MovieManager (it depends on its use). From the mainform you can then just instantiate and show new movie form(s).
We use a piece of code something like this:
static void Main(string[] args)
{
try
{
SplashScreen.ShowSplashScreen();
Application.DoEvents();
SplashScreen.WaitForVisibility(.5);
bool starting = true;
while (starting)
{
try
{
SplashScreen.SetStatus("Initialize mainform...");
starting = false;
Application.Run(new MainForm());
}
catch (Exception ex)
{
if (starting)
starting = XtraMessageBox.Show(ex.Message, "Fout bij initialiseren", MessageBoxButtons.RetryCancel, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1) == DialogResult.Retry;
else
throw (ex);
}
}
}
catch (Exception ex)
{
if (ex is object)
XtraMessageBox.Show(ex.Message, "Algemene fout", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1);
}
}
And code in the splashscreen (excerpt) looks like this:
if (_splashForm == null)
{
ThreadPool.QueueUserWorkItem(RunSplashForm, null);
while (_splashForm == null || _splashForm.IsHandleCreated == false)
{
System.Threading.Thread.Sleep(50);
}
}
Maybe these links will also provide you some usefull information:
http://www.reflectionit.nl/Articles/Splash.aspx
And we used this as the basis for our own code:
http://www.codeproject.com/Articles/5454/A-Pretty-Good-Splash-Screen-in-C
There are different ways to show a new form. you can use MdiWinForm first you must change IsMdiContainer property to true then use these codes in the MainForm :
Form2 f2;
private void button1_Click(object sender, EventArgs e)
{
if (f2 == null) {
f2 = new Form2();
f2.MdiParent = this;
f2.FormClosed += delegate { f2 = null; };
f2.Show();
}
else {
f2.Activate();
}
}

Categories

Resources