How to save the configuration on a MDI Forms Dashboard - vb.net

I'm creating a dashboard (in VB.NET) with multiple modules. Every module is a different form with different functions and settings and the user can create how many and how much of these forms he want. My idea is to make possible to save the general configuration of the Dashboard (example: forms opened, position of these etc.) to be able to recall it when the main form is closed and re-opened and obtain the same situation or change between different settings (different user can load it's own custom preset).
I've tried to serialize the list of forms that are opened but serializing MDI child Forms causes trouble.
All the Forms are ok and works with its own settings. I need only the way to implement this kind of "global settings save".
How can I do this in the most elegant way? (it's ok also a rough idea than I can try getting in deep on my own)
Thanks!

To save the position of all opened MDIChild Windows, you can enumerate the collection of Controls of the MDIParent's MdiClient container (the Container that actually parents the MDI Child Windows).
This will give you the correct order in which these Windows are shown.
The Application.OpenForms collection won't, it just enumerates the opened Forms.
You can use the Form.Name as reference and save its Bounds Rectangle.
Here, I'm using the RectangleConverter class to serialize the Forms' Bounds.
▶ Here, I'm serializing just the Form Name and its Bounds. You can of course serialize whatever other properties / values you need.
In that case, you can build a class structure that stores the information and serialize it using a Json or XML Serializer (I suggest the former. IMO, avoid BinaryFormatter).
To also save the Bounds of the MDIParent, add it last to the list, since when you read back the list of Forms, you have to invert the order in which the Forms are created (the last created goes on top).
To recreate the Forms, you can use Activator.CreateInstance, passing the Type of the Form to create.
The file that stores this information is saved in Application.CommonAppDataPath:
Path.Combine(Application.CommonAppDataPath, "FormsLayout.txt")
It points to a ProgramData folder - dedicated to the calling app - of the drive where the System is installed. Your app always has write permissions here.
When the MDI Application is about to close (Form.FormClosing event handler), the SaveWindowsOrder() method is called. It will store the current Order and Bounds of all opened windows, MDIParent included.
When the MDI Parent is about to be shown (Form.Shown event handler), the LoadWindowsOrder() is called, to restore the previous layout.
Add these Imports:
Imports System.Drawing ' If not already defined in the Project's References
Imports System.IO
Imports System.Linq ' If not already defined in the Project's References
Imports System.Reflection
Public methods
Add to the Form, to a Module or, if you prefer, add Shared and use a dedicated helper class.
Public Sub SaveWindowsOrder(filePath As String, mdiPparent As Form)
Dim formsOrder As New List(Of String)
Dim mClient = mdiPparent.Controls.OfType(Of MdiClient).First()
For Each f As Form In mClient.Controls.OfType(Of Form).ToList()
Dim sRect = New RectangleConverter().ConvertToString(f.Bounds)
formsOrder.Add($"{f.Name};{sRect}")
Next
formsOrder.Add($"{mdiPparent.Name};{New RectangleConverter().ConvertToString(mdiPparent.Bounds)}")
File.WriteAllLines(filePath, formsOrder)
End Sub
Public Sub LoadWindowsOrder(filePath As String, parent As Form)
If Not File.Exists(filePath) Then Return
Dim orderList = File.ReadAllLines(filePath).Reverse().ToArray()
Dim appNameSpace = Assembly.GetExecutingAssembly().GetName().Name
Dim parentData = orderList(0).Split(";"c)
parent.Bounds = CType(New RectangleConverter().ConvertFromString(parentData(1)), Rectangle)
For Each formOrder As String In orderList.Skip(1).ToArray()
Dim params = formOrder.Split(";"c)
Dim formName As String = params(0)
Dim formBounds = CType(New RectangleConverter().ConvertFromString(params(1)), Rectangle)
Dim form = CType(Activator.CreateInstance(Type.GetType($"{appNameSpace}.{formName}")), Form)
form.MdiParent = parent
form.Show()
form.Bounds = formBounds
Next
End Sub
Add to the MDIParent Form:
Private Sub MDIParent1_FormClosing(sender As Object, e As FormClosingEventArgs) Handles MyBase.FormClosing
Dim layoutFile = Path.Combine(Application.CommonAppDataPath, "FormsLayout.txt")
SaveWindowsOrder(layoutFile, Me)
End Sub
Private Sub MDIParent1_Shown(sender As Object, e As EventArgs) Handles MyBase.Shown
Dim layoutFile = Path.Combine(Application.CommonAppDataPath, "FormsLayout.txt")
LoadWindowsOrder(layoutFile, Me)
End Sub

Related

Open Form By Variable Value

Form Name comes from a variable. I would like to open Form from variable value.
In VBA load("UserFormName") will show the form. But I don't know how to do it in VB.Net.
Ok, of course one would want to be able to open a form by string name.
When you create a vb.net winforms project, then all forms are available as a "base" static class.
You often see a LOT of code thus simply use the base form class.
If I need to display say form2, then I really don't need to create a instance of that form (unless you want to have multiple instances of that form. So a truckload of code will simply launch + use the "base static" class of that form.
eg:
Form2.Show()
I don't consider this all that bad of a practice, since what do you think the project settings to "set" the startup form in the project settings does?
It simply sets the built in instance of "mainForm" = to your startup form and it does NOT create new instance.
So, now that we all can agree for 15+ years anyone who has set the startup form in their project is NOT creating a NEW instance of that form, but in fact using the base class instance. This is really a programming choice.
So, code to display (show) the base static instance of a form by string name will look like this:
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim strForm As String = "Form1"
ShowFormByName(strForm)
End Sub
Public Sub ShowFormByName(strFormName As String)
System.Reflection.Assembly.GetExecutingAssembly().CreateInstance(ProductName & "." & strFormName).show()
End Sub
Private Function FormByName(strFormName As String) As Form
Return System.Reflection.Assembly.GetExecutingAssembly().CreateInstance(ProductName & "." & strFormName)
End Function
However, above includes a helper sub that will simply "show" that built in instance of the forms.
And above also includes a function to return class type of the form, since for sure a good many developers prefer to first create a instance of the form, and then "show()" it.
So, often we do want multiple instances, or we just perfer the codeing approach of creating a new instance of the form object.
So, we use the 2nd helper function to return a form object of the type we passed by string.
So, to display 3 instances of form1, but the FIRST instance being the base class, then two more but brand new instances of that form, we have this code:
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim strForm As String = "Form1"
ShowFormByName(strForm)
Dim f1 As Form = FormByName(strForm)
Dim f2 As Form = FormByName(strForm)
f1.Show()
f2.Show()
End Sub
So the above code snip shows how to display the built in base class form without having to create a instance of that form.
However, the next two forms we load are "new" instances of that form as "string".
So the helper sub, and helper function will give you both choices as to which preference floats your boat.
Dim form = System.Reflection.Assembly.GetExecutingAssembly().CreateInstance(Application.ProductName & "." & MySubForm)
Dim frm As New Form
frm = form
frm.MdiParent = AFrmMainScreen
frm.WindowState = FormWindowState.Maximized
frm.Show()
I prefer to use Reflection.Assembly.GetEntryAssembly because I use several different projects in one solution. This allows me to put this code in a different project(dll) that has a usercontrol that I can then reuse across multiple solutions. You also don't need to know the "Namespace" for the form as long as it is in the startup project.
The code below gets the form type from the exported types from the entry assembly and then uses Activator.CreateInstance to create a new instance of the form. Then I return that form in the function.
Public Function GetForm(ByVal objectName As String) As Form
Try
Dim frmType = Reflection.Assembly.GetEntryAssembly.GetExportedTypes.FirstOrDefault(Function(x) x.Name = objectName)
Dim returnForm = TryCast(Activator.CreateInstance(frmType), Form)
Return TryCast(returnForm, Form)
Catch ex As Exception
Return Nothing
End Try
End Function
To use the above function:
Dim MyForm = GetForm(FormLocation)
If MyForm IsNot Nothing Then
MyForm.ShowDialog()
'You can do any form manipulation from here.
Else
MessageBox.Show($"{FormLocation} was not found.")
End If

How do I do stuff with my form objects from another module?

First of all, Why are forms classes?
But now on to the main question.
I have:
Form1.vb
Module1.vb
On the form there is a textbox, progress bar, labels etc.
I want to be able to change the properties of these objects on my form from module1, but I cant seem to access them.
These things on the form are objects, right? So do they have a certain scope? and how can I change it?
Wait but according to my solution explorer, these things are properties of a class??
But the form shows up when I run the program?? Wouldn't the form class have to be instantiated so that the form1 object is created?
Not that it matters, but here is a snippet from module1
Sub WriteObjectsToCSV()
Dim props As PropertyInfo() = MyCompanies(1).GetType().GetProperties()
Dim sw As StreamWriter =
My.Computer.FileSystem.OpenTextFileWriter(SaveAs, False)
Dim csv As New CsvHelper.CsvWriter(sw)
csv.WriteHeader(Of Company)()
csv.NextRecord()
For Each company In MyCompanies
'>>> want to write to my text box and change my progress bar here <<<
For Each prop In props
csv.WriteField(prop.GetValue(company))
Next
csv.NextRecord()
Next
End Sub
Forms are classes because they are created dynamically. You can instantiate and open the same form class serveral times and leave the instances open at the same time.
VB automatically instantiates the main form.
You can access the open forms through My.Application.OpenForms. The main form is always the first
Dim mainForm As Form1
mainForm = DirectCast(My.Application.OpenForms(0), Form1)
mainForm.txtOutput.Text = "hello"
To be able to access the controls of the form from outside, they must be declared Public or Internal. You can change the access modifier from the properties window (Modifiers property).
Instead, you can also declare a property in the form to make the text accessible outside
Public Property OutputText() As String
Get
Return txtOutput.Text
End Get
Set(ByVal value As String)
txtOutput.Text = value
End Set
End Property
Now you can write this in the module
mainForm.OutputText = "hello"

Calling a separate Windows Form using its name as a String

I need to be able to create a Button that can link to a different Windows Form when I click the Button. However, this Button is dynamically generated and can sometimes link to different Forms as per required. For example:
My Button can link to either FormA.vb or FormB.vb. I can make the Button create the String "FormA" or "FormB" as necessary, but I don't know how to call FormA.vb or FormB.vb to the screen.
Thus far, I have been changing Windows Forms by using:
FormA.MdiParent = MainForm //My main form window
FormA.Show()
Me.Close()
But this obviously will not work with:
"FormA".MdiParent = MainForm
"FormA".Show()
Simply because they are Strings and not classes.
Is there a way to make my Button link correctly?
Thanks in advance.
Try this, you have to import System.Windows.Forms and System.Reflection
First get the form name into the strCreatedFromButton then find it.
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim strCreatedFromButton As String = "Form3"
Dim frm As New Form
frm = DirectCast(CreateObjectInstance(strCreatedFromButton), Form)
frm.Show()
End Sub
Public Function CreateObjectInstance(ByVal objectName As String) As Object
Dim obj As Object
Try
If objectName.LastIndexOf(".") = -1 Then
objectName = [Assembly].GetEntryAssembly.GetName.Name & "." & objectName
End If
obj = [Assembly].GetEntryAssembly.CreateInstance(objectName)
Catch ex As Exception
obj = Nothing
End Try
Return obj
End Function
Use a Form type variable to store the reference of either FormA or FormB. Then through polymorphism you can call the Show() method that will execute the appropriate instance's method. For example:
Dim frm as Form
If <SomeCondition> Then
frm = New Form1()
Else
frm = New Form2()
End If
frm.Show()
This is just the core concept. You can extend it to match your exact needs.
Edit
Reading the comments, I'd suggest you just code a large switch (Select Case in VB.NET) for your existing forms and then add new cases for new forms as they're added. You could implement the Factory design pattern to pass your string (e.g. "FormA") to the Factory method and let the factory method return appropriate child class object (again using a switch). To minimize deployment effort, you could keep this Factory class and all new form classes in a separate assembly that will work using simple xcopy deployment.
If you must code it once for all future forms, Reflection is the only way you can do it. However, I'd recommend against it.

Adjust child form's public variable from MDI parent form in VB.NET

I have an MDI parent form that may open a child form called "Order". Order forms have a button that allows the user to print the order. The Order form has a print size variable defined at the beginning:
Public Class Order
Public psize As String
Private Sub button_order_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles process_order.Click
' Code to handle the order and then print, etc
Now the parent form has a psize variable as well, which is set to a default of "A4".
Only when someone clicks on one of the menu items on the Parent window's menu strip will this happen:
psize = "A6"
By default, whenever the parent window opens up a new Order form, I need it to set the child form's psize variable to its own psize value. Something like this:
Dim f As Form
f = New Order
f.MdiParent = Me
f.psize = Me.psize ' BUT THIS LINE DOESN'T WORK
f.Show()
I get the error that f.psize is not a member of the form.
I know that passing variables to and from the MDI parent and child is quite common but despite trying out a few options I saw here, it doesn't seem to be working. Is this the wrong approach?
The reason the property is not available is because you are using the wrong type for the variable. The base Form type does not define that property. Instead, your derived Order type does. You could do something like this:
Dim f As Order
f = New Order
f.MdiParent = Me
f.psize = Me.psize
f.Show()
UPDATE
As you have said in comments below, what you really need to do is to be able to share a dynamic setting between all your forms so that you can change the setting at any time and have it affect all your forms that have already been displayed. The best way to do that, would be to create a new class that stores all your shared settings, for instance:
Public Class Settings
Public PaperSize As String = "A6"
End Class
As you can see, by doing so, you can easily centralize all your default settings in your settings class, which is an added benefit. Then, you need to change the public property in your Order form to the new Settings type, for instance:
Public Class Order
Inherits Form
Public Settings As Settings
End Class
Then, you need to create your shared settings object in your MDI Parent form, and then pass it it to each of your Order forms as they are created:
Public Class MyParentForm
Private _settings As New Settings()
Private Sub ShowNewOrderForm()
Dim f As New Order()
f.MdiParent = Me
f.Settings = _settings
f.Show()
End Sub
Private Sub ChangePaperSize(size As String)
_settings.PaperSize = size
End Sub
End Class
Then, since the parent form and all the child Order forms share the same Settings object, and change made to that Settings object will be seen immediately by all the forms.
Change this:
Dim f As Form
to the actual implementation of your form:
Dim f As Order
or just the shortcut:
Dim f As New Order

VS 2010 Ultimate VB.NET Project form won't compile/show up

When press F5 to compile a project, there are no errors or warnings but the form won't show up. What's up?
Every time that you try to run your code, it starts by creating an instance of frmMain, your default form and the one that is shown at application startup.
When this form is created, it automatically instantiates an instance of Form3 because you instantiate a variable of that type called modifyForm at the top of this form's code file:
Dim modifyForm As New Form3 'modify student
The problem is that, when the runtime goes to instantiate an object of type Form3, it gets called right back to where it was because of this statement at the top of the Form3 code file:
Dim frmMain As New frmMain
Rinse, lather, and repeat. Your code turns into an infinite loop, trying to instantiate an instance of frmMain, which tries to instantiate an instance of Form3, which tries to instantiate an instance of frmMain, ad nauseum. Eventually, this will overflow your available memory and cause a StackOverflowException.
It's important to note that all of this happens before the default instance of frmMain is even shown, because these variables are declared outside of any methods in your code. Because the computer never can escape this infinite loop, it never gets a chance to move on and actually display your form.
And the moment you've all been reading so patiently for:
Fix it by removing the declaration of frmMain at the top of the Form3 code file. I don't know what that's there for, anyway.
EDIT: Hopefully, I can clear up a little confusion regarding passing values between forms. Upon further study of your code, my instincts tell me that the best solution for you is to overload the constructor for Form3 to accept the calling form (the existing instance of frmMain) as an argument. This will allow you to access all of the public members of frmMain from within your Form3 class.
Here's a rough sketch of how you might do this in your code:
Public Class frmMain
''#The private data field that stores the shared data
Private _mySharedData As String = "This is the data I want to share across forms."
''#A public property to expose your shared data
''#that can be accessed by your Form3 object
Public Property MySharedData As String
Get
Return _mySharedData
End Get
Set(ByVal value As String)
_mySharedData = value
End Set
End Property
Private Sub frmMain_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
''#Do all of your other stuff here...
''#Create a new instance of Form3, specifying this form as its caller
Dim otherForm As New Form3(Me)
''#Show the new instance of Form3
otherForm.Show()
End Sub
End Class
Public Class Form3
''#The private field that holds the reference to the main form
''#that you want to be able to access data from
Private myMainForm As frmMain
Public Sub New(ByVal callingForm As frmMain)
InitializeComponent()
''#Save the reference to the calling form so you can use it later
myMainForm = callingForm
End Sub
Private Sub Form3_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
''#Access anything that you need from the main form
MessageBox.Show(myMainForm.MySharedData)
End Sub
End Class
As you can see, frmMain exposes a public property (backed by a correspondingly-named private variable) called MySharedData. This can be absolutely anything you want, and you can have as many of these as you want.
Also notice that how the constructor (the New method) for Form3 accepts an instance of frmMain as an argument. Whenever you create a new Form3 object from frmMain, you just specify Me, which indicates that you want to pass the current instance of frmMain. In the constructor method, Form3 stores that reference to its calling form away, and you can use this reference any time you like in Form3's code to access the public properties exposed by frmMain.
In VS2010 menu, go to Build -> Configuration Manager, does your project have the checkbox in the "Build" column enabled?
If it's project upgraded from an older Visual Studio version it may be that it is not targeting .NET Framework 4.0. In that case you should change it as explained here.
To analyze the problem press F8 (or F10, depends on your default keyboard settings) to step into the code instead of running the app. This should take you to the main method where the main form would be initialized.