Custom CollectionEditor never triggers a property's "set" method - vb.net

I am trying to implement a way of persisting a collection in a custom settings class. I have successfully created the settings class (inheriting ApplicationSettingsBase) and can save properties using the built-in editors on a PropertyGrid, but my custom implementation of a property grid for collections doesn't persist any of the values I add. Here's my code:
Imports System.Configuration
Imports System.ComponentModel
Imports System.Drawing.Design
Imports System.ComponentModel.Design
Public Class CustomSettings
Inherits ApplicationSettingsBase
<UserScopedSetting()> _
<DefaultSettingValue("White")> _
Public Property BackgroundColor() As Color
Get
BackgroundColor = Me("BackgroundColor")
End Get
Set(ByVal value As Color)
Me("BackgroundColor") = value
End Set
End Property
<UserScopedSetting()> _
<DesignerSerializationVisibility(DesignerSerializationVisibility.Content)> _
<Editor(GetType(CustomStringCollectionEditor), GetType(UITypeEditor))> _
Public Property EmailAddresses() As Collection
Get
EmailAddresses = Me("EmailAddresses")
End Get
Set(ByVal value As Collection)
Me("EmailAddresses") = value
End Set
End Property
End Class
Public Class CustomStringCollectionEditor
Inherits CollectionEditor
Public Sub New()
MyBase.New(GetType(Collection))
End Sub
Protected Overrides Function CreateInstance(ByVal itemType As System.Type) As Object
Return String.Empty
End Function
Protected Overrides Function CreateCollectionItemType() As System.Type
Return GetType(String)
End Function
End Class
I set a breakpoint on the Set methods for both the BackgroundColor property and the EmailAddresses property. The BackgroundColor property works as it should - it breaks on the Set statement and stores the property correctly. But when I close the custom CollectionEditor dialog, the EmailAddresses "Set" method is never called. How can I get my custom editor to actually save the property once it's done being edited?

I think I fixed it (with help from this question). I added an override to the EditValue function in my custom editor. Here is the code:
Public Overrides Function EditValue(ByVal context As System.ComponentModel.ITypeDescriptorContext, ByVal provider As System.IServiceProvider, ByVal value As Object) As Object
Dim result As Object = MyBase.EditValue(context, provider, value)
DirectCast(context.Instance, CustomSettings).EmailAddresses = DirectCast(result, List(Of String))
Return result
End Function
I also moved from a collection to a list - I read somewhere that was a safer way to go. I also added a constructor to my CustomSettings class that set the EmailAddresses property to a new List(Of String) if it was unset to begin with. I found that the first time it ran, I could edit the list and add items, but they wouldn't be persisted:
Public Sub New()
If Me("EmailAddresses") Is Nothing Then
Me("EmailAddresses") = New List(Of String)
End If
End Sub
And now it's all working like it should. But if this isn't the best way or there's an easier way to do it, please chime in.

Related

How to add a user control drop down property in VB.NET

I am using a class library in Visual Basic 2010 Express Edition to make a custom textbox control. How do I add a dropdown property for the textbox?
I need a dynamic dropdown menu not like when you using
Enum MaxValue
item1 = 0
End Enum
because I have to get the items from the database.
I tried adding a browsable option but nothing happened:
<Browsable(True)>
Property Max_Value() As String
Get
Return MaxValue
End Get
Set(value As String
MaxValue = value
End Set
End Property
Imports System.ComponentModel
Imports System.Drawing.Design
Imports System.Windows.Forms.Design
Public Class TestTextBox
Inherits TextBox
<Browsable(True)>
<Editor(GetType(Editor), GetType(UITypeEditor))>
<DefaultValue("Hello")>
Public Property MyProperty As String
Private Class Editor
Inherits UITypeEditor
Private mSvc As IWindowsFormsEditorService
Public Overrides Function GetEditStyle(context As ITypeDescriptorContext) As UITypeEditorEditStyle
Return UITypeEditorEditStyle.DropDown
End Function
Public Overrides Function EditValue(context As ITypeDescriptorContext, provider As IServiceProvider, value As Object) As Object
mSvc = CType(provider.GetService(GetType(IWindowsFormsEditorService)), IWindowsFormsEditorService)
Dim lb As New ListBox()
For Each value In {"Hello", "Whats", "Happening"}
lb.Items.Add(value)
Next
If value IsNot Nothing Then
lb.SelectedItem = value
End If
mSvc.DropDownControl(lb)
value = DirectCast(lb.SelectedItem, String)
Return value
End Function
End Class
End Class

VB.NET Forms ListBox doesn't display DataSource

I'm trying to link my ListBox to an ObservableCollection.
Here's my class for defining mods:
Public Class TroveMod
Private m_FileName As String
Private m_Enabled As Boolean
Public Property FileName() As String
Get
Return m_FileName
End Get
Set(value As String)
m_FileName = value
End Set
End Property
Public Property Enabled() As Boolean
Get
Return m_Enabled
End Get
Set(value As Boolean)
m_Enabled = value
End Set
End Property
Public ReadOnly Property ModName()
Get
Return Path.GetFileNameWithoutExtension(FileName)
End Get
End Property
End Class
And this is the actual Property ModList:
Public Property ModList() As ObservableCollection(Of TroveMod)
Get
Return m_ModList
End Get
Set(value As ObservableCollection(Of TroveMod))
m_ModList = value
End Set
End Property
I add items using:
Private Sub AddMod(file__1 As String, enabled As Boolean)
If File.Exists(file__1) Then
ModList.Add(New TroveMod() With { _
.FileName = file__1, _
.Enabled = enabled _
})
End If
End Sub
Everytime I want to add something to this Collection using AddMod, it won't show off in my listbox :/ I added an ModListBindingSource to the ListBox and set the DisplayMember and ValueMember to ModName but it still won't work. I also got a status label, which says, that it successful added the mods to the collection but it simply won't show them in the ListBox. Did I miss something?
An ObservableCollection is not quite what you want - this would allow your code to see changes to the collection via events. For instance, if there were several actors adding items to the collection and you wanted to know that. In this case, since 'local' code is adding items, it is not needed.
It is also not clear how the collection is mapped to the ListBox as the DataSource. Try this:
Public Class Form...
Private myList As New BindingList(Of TroveMod)
Sub Form_Load(....
theListBox.DataSource = myList
theListBox.DisplayMember = "ModName"
Now, as you add things to the BindingList they will appear in theListBox. If an item changes though (e.g. the Name), that change will not show up without a little more work, but as these are file names that seems unlikely.
The form property is not needed unless an external class also needs access to the collection/BindingList; in that case, you probably do not want need a setter.

Get a Field Object, not FieldInfo, from a VB Class Instance

I am trying to iterate through objects (fields) in a class and invoke a method on each object. Each object is of a different type. Here is the parent class:
Public Class MySettings
Public IdentifyByFacType As RadioButtonSetting
Public WtrFacTypes As ListSetting
Public OilFacTypes As ListSetting
Public GroupByRef As CheckboxSetting
Public GroupRefAttr As TxtboxSetting
End Class
Here is part of one of the sub-object classes:
<Serializable>
Public Class TxtboxSetting
<XmlIgnore()>
Public MyControl As Windows.Forms.TextBox
<XmlIgnore()>
Public DefaultSetting As String
Private _SavedSetting As String
Public Property SavedSetting As String
Get
Return _SavedSetting
End Get
Set(value As String)
_SavedSetting = value
CurrentValue = value
End Set
End Property
Public Sub New()
End Sub
Public Sub New(DefaultSetting As String, MyControl As Windows.Forms.TextBox)
Me.DefaultSetting = DefaultSetting
Me.MyControl = MyControl
End Sub
Public Sub RestoreDefault()
CurrentValue = DefaultSetting
End Sub
End Class
All of the sub-objects of the MySettings class, like GroupRefAttr for example, have the same methods and properties, but the internal code is different.
So I will have several classes like the MySettings class, and each one will have different sub-objects. Given an instance of such a class, I want to automatically iterate through the fields and call a method RestoreDefault on each one. I don't want to have to know what objects exist in the MySettings class. Rather, knowing that they all have the RestoreDefaultmethod, I want simply call the method on each object.
Despite much searching, I have not found a way to do this. With reflection, I can only get this far:
Dim Opts as New MySettings
For Each var In Opts.GetType.GetFields
Dim RestoreDefault As System.Reflection.MethodInfo = var.FieldType.GetMethod("RestoreDefault")
RestoreDefault.Invoke(Opts, Nothing)
Next
However, in the line RestoreDefault.Invoke(Opts, Nothing), I can't just pass in Opts, as I am dealing with a field in Opts, not Opts itself. A statement like this would work: RestoreDefault.Invoke(Opts.GroupRefAttr, Nothing), but that requires me to know the objects in the MySettings class ahead of time, and that defeats the purpose. Is there a way to grab field instance objects at runtime and pull this off?
When you invoke the RestoreDefault method you need to invoke it on the setting (i.e., the value of the field), not the class containing the setting. Changing your code to this should fix your problem:
Dim Opts as New MySettings
For Each var In Opts.GetType.GetFields
Dim setting As Object = var.GetValue(Opts)
Dim RestoreDefault As System.Reflection.MethodInfo = var.FieldType.GetMethod("RestoreDefault")
RestoreDefault.Invoke(setting, Nothing)
Next
However, if you introduce either a base class or an interface you should be able to get rid of some or all of the reflection. The container setting class can have a collection of settings that each have a shared base class or interface with a RestoreDefault method. The container setting class will then call this method through the base class or interface without having to use reflection.
The base class:
Public MustInherit Class BaseSetting
Public MustOverride Sub RestoreDefault
End Class
A specific settings class:
Public Class TxtboxSetting
Inherits BaseSetting
Public Overrides Sub RestoreDefault()
' Specific implementation
End Sub
End Class
On any class deriving from BaseSetting you can now call the RestoreDefault method without having to use reflection.
However, considering your design you might still want to use reflection to get the settings containd in the MySettings class. You can do it like this:
Dim settings = From fieldInfo in Opts.GetType.GetFields
Where GetType(BaseSetting).IsAssignableFrom(fieldInfo.FieldType)
Select DirectCast(fieldInfo.GetValue(Opts), BaseSetting)
For Each setting In settings
setting.RestoreDefault()
Next
Reflection is used to find all the fields deriving from BaseSetting and then RestoreDefault is called on each field. This method does not suffer from the "magic string" problem where your code depends on the name of the RestoreDefault method represented in a string.
(Also, calling the MySettings class the parent is a bit misleading because there is nothing inheriting from MySettings. Instead this class contains other settings.)
All of the sub-objects of the MySettings class, like GroupRefAttr for example, have the same methods and properties, but the internal code is different.
In that case, the sub-object types should be defined such that they implement a common interface that demands these same methods and properties exist. For now, I'll name that interface IControlSetting. Then, your For loop looks something like this:
Dim Opts as New MySettings
For Each var In Opts.GetType.GetFields
Dim setting As IControlSetting = TryCast(var.GetValue(Opts), IControlSetting)
If setting Is Nothing Then Continue
setting.RestoreDefault()
Next
Additionally, I'd change your MySettings type to encapsulate a dictionary or IControlSetting objects. Then you can just iterate the dictionary to check each of the objects, rather than needing reflection. That might look like this:
Public Class MySettings
Private allSettings As Dictionary(Of String, IControlSetting)
Public Sub New()
allSettings = new Dictionary(Of String, IControlSetting)()
allSettings.Add("IdentifyByFacType", New RadioButtonSetting())
allSettings.Add("WtrFacTypes", New ListSetting())
allSettings.Add("OilFacTypes", New ListSetting())
'...
End Sub
Public Property IdentifyByFacType As RadioButtonSetting
Get
Return DirectCast(allSettings("IdentifyByFacType"), RadioButtonSetting)
End Get
'The setters may be optional, depending on how you expect to use these
Set(ByVal value As RadioButtonSetting)
allSettings("IdentifyByFacType") = value
End Set
End Property
Public Property WtrFacTypes As ListSetting
Get
Return DirectCast(allSettings("WtrFacTypes"), RadioButtonSetting)
End Get
Set(ByVal value As ListSetting)
allSettings("WtrFacTypes") = value
End Set
End Property
Public Property OilFacTypes As ListSetting
Get
Return DirectCast(allSettings("OilFacTypes"), RadioButtonSetting)
End Get
Set(ByVal value As ListSetting)
allSettings("OilFacTypes") = value
End Set
End Property
'...
Public Sub RestoreAllDefaults()
For Each setting As KeyValuePair(Of String, IControlSetting) In allSettings
setting.Value.RestoreDefault()
Next setting
End Sub
End Class

How to do a collection property inside another collection in user control

I have a user control with a property "Rules" that is a generic list.
Every "rule" is associated to a combobox control and i have to create a property to host data for the combobox. I used another generic list to accomplish this.
In design works well, i can add items normally in property grid, but when i run the program the values are not maintained.
Rules property:
Private _regras As New List(Of ParametrosColunasGrid)
<Category("Ecletica")> _
<Browsable(True)> _
<System.ComponentModel.DesignerSerializationVisibility(System.ComponentModel.DesignerSerializationVisibility.Content)>
Public Property Regras() As List(Of ParametrosColunasGrid)
Get
Return _regras
End Get
Set(value As List(Of ParametrosColunasGrid))
_regras = value
End Set
End Property
Public Class ParametrosColunasGrid
'...
Private _itens_Combo As New List(Of ItemComboBox)
<DesignerSerializationVisibility(DesignerSerializationVisibility.Content)>
Public Property ItensCombo As List(Of ItemComboBox)
Get
Return _itens_Combo
End Get
Set(value As List(Of ItemComboBox))
_itens_Combo = value
End Set
End Property
'...
End Class
ItemComboBox Class:
<Serializable()>
Public Class ItemComboBox
Public Property Value As String
Public Property Key As String
Public Overrides Function ToString() As String
Return _Value
End Function
End Class

custom categoryattribute for control shadowed property

I made a custom CategoryAttribute in order to localize properties of custom controls.
<AttributeUsage(AttributeTargets.Property)> _
Public Class LocalisableCategoryAttribute
Inherits CategoryAttribute
Public Sub New(ByVal resourceName As String)
MyBase.New(resourceName)
End Sub
Protected Overrides Function GetLocalizedString(value As String) As String
Return My.Resources.ResourceManager.GetString(value)
End Function
End Class
My custom controls have both brand new properties with this attribute but I also shadowed some of the "basic" properties (such as the Size and Location) in order to give them this attribute.
<LocalisableCategory("Category_Apparence")> _
Public Shadows Property Size As Size
Get
Return MyBase.Size
End Get
Set(value As Size)
MyBase.Size = value
End Set
End Property
The big problem is that at runtime, sometimes the new "Apparence" category will show up and sometimes the old "Layout" one will. Only shadowed properties have this odd behavior. It's totally random. It's not a compile thing either. You can launch the .exe twice in a row and the property grid won't show the same result. Sometimes it will be put under its old category, sometimes the new localized one.
I am completely at a loss with this since it's so random. Can anyone help?
You have to change:
<AttributeUsage(AttributeTargets.Property)> _
Public Class LocalisableCategoryAttribute
Inherits CategoryAttribute
Private resourceKey As String
Public Sub New(ByVal resourceName As String)
resourceKey = resourceName
End Sub
Protected Overrides Function GetLocalizedString(value As String) As String
Return My.Resources.ResourceManager.GetString(resourceKey)
End Function
End Class