|
-
Jul 8th, 2010, 02:27 AM
#1
[RESOLVED] Making a Component "one per Form"
I'm making a custom Toolbox Component which I want to restrict to one instance per form. It's a non-visual component like a Timer, not a control. I am able to enforce the restriction at design time by checking if the Container.Components list already contains a component of the same type.
But I want to do something similar when someone adds an instance of the component in code. It's easy in the case of a Control because you can check in Form.Controls. Unfortunately the Controls list doesn't contain non-visual components.
Does anyone know a way of getting a list of a form's non-visual components? They are obviously present in the form's designer.vb file, but I don't know how to access that in code. Or can someone suggest another way to restrict the component to one per form?
Note that I don't want to place special requirements on the form itself. For example, I considered using the Form's Tag property to set a flag if it already contains an instance of the component. But that precludes using the Tag for some other purpose.
BB
Last edited by boops boops; Jul 8th, 2010 at 02:33 AM.
Reason: always a typo!
-
Jul 8th, 2010, 03:15 AM
#2
Re: Making a Component "one per Form"
 Originally Posted by boops boops
But I want to do something similar when someone adds an instance of the component in code.
What does that actually mean? If I do this:
vb.net Code:
Dim obj As New SomeComponent
I'm not "adding" an instance to anything. I'm simply creating an instance. That code might be in a form or in some other class, so your restriction is not really meaningful. It's really only if you do actually add the component to the form's collection of components, which noone is ever going to do at run time.
-
Jul 8th, 2010, 03:58 AM
#3
Re: Making a Component "one per Form"
 Originally Posted by jmcilhinney
What does that actually mean? If I do this:
vb.net Code:
Dim obj As New SomeComponent
I'm not "adding" an instance to anything. I'm simply creating an instance. That code might be in a form or in some other class, so your restriction is not really meaningful. It's really only if you do actually add the component to the form's collection of components, which noone is ever going to do at run time.
Sorry that I didn't make that clear. My component has a close relation with the form that hosts it. Among other things it sets various form properties to default values. The compile-time constructor therefore requires a Form as an argument, e.g.
Code:
Dim obj As New SomeComponent(Me)
I'm trying to deal with the situation where the programmer has dragged a SomeComponent onto a Form in the Designer, and then declares one or more SomeComponents for that form in code. I could just say "don't do it" but I would prefer to throw an appropriate exception.
BB
Last edited by boops boops; Jul 8th, 2010 at 04:08 AM.
-
Jul 8th, 2010, 05:01 AM
#4
Re: Making a Component "one per Form"
How can your Component class require a parameter in its constructor? Then you wouldn't be able to drop it on a form in the first place. Unless it also has a parameterless constructor, in which case: what stops the user from using that one?
Anyway, I guess the solution is pretty simple: get a list of all components on the form, and check if it contains one of the same type as your component. The problem of course is how to get that list...
You can get to it via the Site.Container.Components property of the Form. There is a problem though: the Site property is often Nothing, and I don't think there's any events that you can use to react when it gets its value.
But you can override the Site property of the Component, and when it changes you can get the parent form of your component. From that, you can get to the components.
Something like this works for me, during Design-time:
vb.net Code:
Imports System.ComponentModel Imports System.ComponentModel.Design Public Class TestComponent Inherits Component Private _ParentForm As Form Public Property ParentForm() As Form Get Return _ParentForm End Get Set(ByVal value As Form) _ParentForm = value ' ParentForm changed, check for other components here Me.CheckComponents() End Set End Property Private Sub CheckComponents() If Me.ParentForm IsNot Nothing Then If Me.ParentForm.Site IsNot Nothing AndAlso Me.ParentForm.Site.Container IsNot Nothing Then For Each c As Component In Me.ParentForm.Site.Container.Components If TypeOf c Is TestComponent AndAlso c IsNot Me Then Throw New Exception("Duplicates!") End If Next End If End If End Sub 'Override the Site property to know when it changes so you can get the parent form Public Overrides Property Site() As System.ComponentModel.ISite Get Return MyBase.Site End Get Set(ByVal value As System.ComponentModel.ISite) MyBase.Site = value If value Is Nothing Then Return Dim host = TryCast(value.GetService(GetType(IDesignerHost)), IDesignerHost) If host IsNot Nothing Then Dim componentHost = host.RootComponent If componentHost IsNot Nothing Then Me.ParentForm = TryCast(componentHost, Form) End If End If End Set End Property End Class
It does not allow me to add two instances of TestComponent to a form. It throws an exception and consequently does not create the component.
I don't know how to handle your other method of creating the component (via code). I tried running the same code in the constructor (the one that takes a Form parameter), but the Site property of the Form is then Nothing and you cannot get to the components. No idea why...
-
Jul 8th, 2010, 05:42 AM
#5
Re: Making a Component "one per Form"
Hi Nick,
The component does indeed have a parameterless constructor for design time. It is empty so if someone uses it in code it simply has no action. I have no problem with that.
I am already doing something very similar to what you have suggested for enforcing "one per form" at design time. Of course I take advantage of your earlier help with getting the parent form through the Site property. Instead of throwing an exception, I quietly copy the properties of the existing component and then dispose it. It works almost perfectly: the icon in the Components tray blinks ever so slightly.
If neither John nor you know of a way of getting a list of the form's non-visible components at run time, then I'm prepared to believe one does not exist. I do have other options. I could upgrade the component to do something useful with multiples, although I don't think it would be worth the effort. I could disallow instantiation in code by deleting the constructor with a parameter. Or I could tolerate multiple instances of the component and disclaim responsibility for the consequences (which will probably be mainly cosmetic).
thanks, BB
-
Jul 8th, 2010, 05:48 AM
#6
Re: Making a Component "one per Form"
I can't imagine it to be impossible to get a list of the components. I am asking on a different forum, perhaps they know some answers.
-
Jul 8th, 2010, 07:30 AM
#7
Re: Making a Component "one per Form"
Couldn't your component keep a Shared list of forms it's been added to and then throw an exception if the same form is added twice?
-
Jul 8th, 2010, 07:57 AM
#8
Re: Making a Component "one per Form"
-
Jul 8th, 2010, 08:17 AM
#9
Re: Making a Component "one per Form"
 Originally Posted by Grimfort
Looks promising for design-time, but the problem is really during run-time. I'm sure you can still create a new instance of the component in code using this approach.
 Originally Posted by jmcilhinney
Couldn't your component keep a Shared list of forms it's been added to and then throw an exception if the same form is added twice?
Good idea. That may be the best solution yet, in combination with Grimfort's link.
-
Jul 8th, 2010, 08:45 AM
#10
Re: Making a Component "one per Form"
Using a Shared list is one of the first things I tried. It is certainly possible to build such a list at design time (although I found Container.Components easier to use for the purpose of eliminating an existing component). Unfortunately, it turns out that the list is empty at run time. I guess the designer does not serialize Shared properties.
I don't have much experience with serialization, but perhaps it is possible to enforce serialization of the Shared List(Of Form) in some way.
BB
-
Jul 9th, 2010, 08:40 AM
#11
Re: Making a Component "one per Form"
I've found something useful about getting a list of components at runtime:Enumerating non-ui components from the given form instance on DotNetMonster. I made this little function based on one of the postings:
vb.net Code:
Imports System.ComponentModel
Imports System.Reflection
' ....
Public Function ComponentList(ByVal frm As Form) As ComponentCollection
Dim fieldInfo As FieldInfo = frm.GetType.GetField("components", BindingFlags.NonPublic Or BindingFlags.Instance)
If fieldInfo IsNot Nothing Then
Dim container As IContainer = TryCast(fieldInfo.GetValue(frm), IContainer)
If container IsNot Nothing Then Return container.Components
End If
Return Nothing
End Function
It works -- a bit. The limitations are:
1. It only returns components created in the Designer. (That's not a problem for my present purposes, since I can prevent multiple instantiations at run time by setting a Shared Boolean flag.)
2. It includes some components but not others. Timer, ImageList and SerialPort are included, but BackgroundWorker and HelpProvider are not. Nor is my custom component .
The above thread includes a more complex method which may turn out to return a more complete collection. I'll report my findings on that when I have had time to try it out, if no one else does first.
BB
-
Jul 11th, 2010, 12:30 PM
#12
Re: Making a Component "one per Form"
The "full" version used an Enumerator class to access the components field, but the same basic method for extracting components. Obviously it suffered from the same defects. It seems that the form's components field, besides being hard to get at, only contains certain components.
Time to think of another way. Components created at design time are all declared in the form's designer.vb file. Since designer.vb is a Partial Class of the form class, a given form's declared objects will include both design time and code instances. So I looked into Reflection for a way of getting the fields of a class (Form1) and came up with this:
vb.net Code:
Public Function InstancesOf(SomeComponent As Component) As Integer
Dim count as integer = 0
Dim fieldInfos() As FieldInfo = Form1.GetType.GetFields _
(BindingFlags.NonPublic Or BindingFlags.Instance)
For Each f As FieldInfo In fieldInfos
If f.FieldType Is GetType(SomeComponent)) Then count+=1
Next
Return count
End Function
This successfully returns the number of declared instances of my custom component, whether created in the designer or in code. So I can use it to throw an exception as required. Thread resolved.
GetFields can also be used to make a list of all the components on a form. I'll post a function for that to the CodeBank.
BB
Posting Permissions
- You may not post new threads
- You may not post replies
- You may not post attachments
- You may not edit your posts
-
Forum Rules
|
Click Here to Expand Forum to Full Width
|