How do I create my own custom property page to handle a property of my own custom type?

Robert Gustafson 606 Reputation points
2022-07-23T03:42:52.093+00:00

WHAT I HAVE:
Visual Basic 2019, .NET Framework 4.0+/.NET Core, WinForms

MY PROBLEM:
As I progressively tweeked my extended custom controls, I've found myself creating public properties with 1 or more parameters, of custom types (structures or classes I've created with various sub-properties), and of object collections whose elements are of custom types. While this facilitates a host program's ability to manipulate the control at run time, it complicates making these properties available to update a design time through the properties window.

The solution, I believe, is to design property pages. But most of the documentation in creating custom property pages involve variations of pre-existing page styles using pre-existing primitive types. I need to be able to create a property page that's a custom modal form which handles a property of a custom type--thereby giving me complete control over the behavior and appearance of the page. I'd like an example that's general-purpose, in VB.NET, and relatively simple showing me how my basic concept would work. (Once shown the basic model, I can create a more specific thing therefrom.)

I'm also interested if it's possible to make a property that takes 1 or more required parameters browsable in the properties window, as well as whether it's possible to tweak the property-page type that handles "collection" properties (like properties whose types are arrays, or derive from the ObjectCollection type of ListBox/ComboBox) so that the page expects each element to be of a specific type and exposes the underlying sub-properties of the class/structure for modification at design-time. (Or must I design my own property page? All the more reason to show me a general-purpose example.)

Please answer ASAP. Ideally, I'd like a basic example, not just links.

Windows Forms
Windows Forms
A set of .NET Framework managed libraries for developing graphical user interfaces.
1,835 questions
VB
VB
An object-oriented programming language developed by Microsoft that is implemented on the .NET Framework. Previously known as Visual Basic .NET.
2,579 questions
.NET Runtime
.NET Runtime
.NET: Microsoft Technologies based on the .NET software framework.Runtime: An environment required to run apps that aren't compiled to machine language.
1,125 questions
0 comments No comments
{count} vote

Accepted answer
  1. Reza Aghaei 4,936 Reputation points MVP
    2022-07-23T18:38:54.507+00:00

    Here are the summary of your questions:

    1. Basic concepts of a custom property editor and Example
    2. If it's possible to make a property that takes 1 or more required parameters
    3. Whether it's possible to tweak the property-page type that handles "collection" properties

    Basic concepts of a custom property editor and Example

    Assuming you want to have a custom property editor for a property, then you need to do the following steps:

    1. Create a custom property editor by inheriting from UITypeEditor
    2. Override GetEditStyle and return UITypeEditorEditStyle.Modal or UITypeEditorEditStyle.DropDown. An example of a modal editor is the modal that you see while editing a Font. An example of a dropdown editor is the dropdown that you see while editing Anchor or Dock.
    3. Override EditValue and show the dropdown or show the modal to edit the value.
    4. Decorate your property with the EditorAttribute

    To learn more, take look at:

    Example - Modal property editor for a complex property

    Here is a step by step example which shows you how to create a modal property editor for a complex property. In this example we create a control which has a Boder property which has two properties, BorderWidth and BorderColor. So when editing the property, you see a [...] and if you click on it, you see the following modal:

    224051-image.png

    223950-image.png

    Here are the steps:

    1. Create a Windows Forms project
    2. Add a reference to System.Design
    3. Add a class called Border.vb with the following content: Public Class Border
      Private m_borderWidth As Integer
      Public Property BorderWidth() As Integer
      Get
      Return m_borderWidth
      End Get
      Set(ByVal value As Integer)
      ' Valiate the value and throw new Exception if it's not valie
      m_borderWidth = value
      End Set
      End Property
      Private m_borderColor As Color
      Public Property BorderColor() As Color
      Get
      Return m_borderColor
      End Get
      Set(ByVal value As Color)
      ' Valiate the value and throw new Exception if it's not valie
      m_borderColor = value
      End Set
      End Property
      End Class
    4. Add a class called BorderEditorForm.vb with the following content:
      Public Class BorderEditorForm  
      Inherits Form  
      Friend WithEvents borderColorComboBox As ComboBox  
      Private borderColorLabel As Label  
      Private borderWidthNumericUpDown As NumericUpDown  
      Private borderWidthLabel As Label  
      Private saveButton As Button  
      Private rejectButton As Button  
      Friend WithEvents tableLayoutPanel1 As TableLayoutPanel  
      Friend WithEvents flowLayoutPanel1 As FlowLayoutPanel  
      Private m_border As Border  
      Public Property Border() As Border  
          Get  
              Return New Border() With {  
                  .BorderColor = CType(borderColorComboBox.SelectedItem, Color),  
                  .BorderWidth = borderWidthNumericUpDown.Value}  
          End Get  
          Set(ByVal value As Border)  
              m_border = value  
              borderWidthNumericUpDown.Value = value.BorderWidth  
              borderColorComboBox.SelectedItem = value.BorderColor  
          End Set  
      End Property  
      Private Sub InitializeComponent()  
          Me.borderColorComboBox = New System.Windows.Forms.ComboBox()  
          Me.borderColorLabel = New System.Windows.Forms.Label()  
          Me.borderWidthNumericUpDown = New System.Windows.Forms.NumericUpDown()  
          Me.borderWidthLabel = New System.Windows.Forms.Label()  
          Me.saveButton = New System.Windows.Forms.Button()  
          Me.rejectButton = New System.Windows.Forms.Button()  
          Me.tableLayoutPanel1 = New System.Windows.Forms.TableLayoutPanel()  
          Me.flowLayoutPanel1 = New System.Windows.Forms.FlowLayoutPanel()  
          CType(Me.borderWidthNumericUpDown, System.ComponentModel.ISupportInitialize).BeginInit()  
          Me.tableLayoutPanel1.SuspendLayout()  
          Me.flowLayoutPanel1.SuspendLayout()  
          Me.SuspendLayout()  
          '  
          'borderColorComboBox  
          '  
          Me.borderColorComboBox.Anchor = CType(((System.Windows.Forms.AnchorStyles.Top Or System.Windows.Forms.AnchorStyles.Left) _  
              Or System.Windows.Forms.AnchorStyles.Right), System.Windows.Forms.AnchorStyles)  
          Me.borderColorComboBox.DrawMode = System.Windows.Forms.DrawMode.OwnerDrawFixed  
          Me.borderColorComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList  
          Me.borderColorComboBox.Location = New System.Drawing.Point(220, 20)  
          Me.borderColorComboBox.Margin = New System.Windows.Forms.Padding(10)  
          Me.borderColorComboBox.Name = "borderColorComboBox"  
          Me.borderColorComboBox.Size = New System.Drawing.Size(552, 39)  
          Me.borderColorComboBox.TabIndex = 1  
          '  
          'borderColorLabel  
          '  
          Me.borderColorLabel.AutoSize = True  
          Me.borderColorLabel.Location = New System.Drawing.Point(20, 20)  
          Me.borderColorLabel.Margin = New System.Windows.Forms.Padding(10)  
          Me.borderColorLabel.Name = "borderColorLabel"  
          Me.borderColorLabel.Size = New System.Drawing.Size(175, 32)  
          Me.borderColorLabel.TabIndex = 0  
          Me.borderColorLabel.Text = "Border Color"  
          '  
          'borderWidthNumericUpDown  
          '  
          Me.borderWidthNumericUpDown.Anchor = CType(((System.Windows.Forms.AnchorStyles.Top Or System.Windows.Forms.AnchorStyles.Left) _  
              Or System.Windows.Forms.AnchorStyles.Right), System.Windows.Forms.AnchorStyles)  
          Me.borderWidthNumericUpDown.Location = New System.Drawing.Point(220, 79)  
          Me.borderWidthNumericUpDown.Margin = New System.Windows.Forms.Padding(10)  
          Me.borderWidthNumericUpDown.Maximum = New Decimal(New Integer() {50, 0, 0, 0})  
          Me.borderWidthNumericUpDown.Name = "borderWidthNumericUpDown"  
          Me.borderWidthNumericUpDown.Size = New System.Drawing.Size(552, 38)  
          Me.borderWidthNumericUpDown.TabIndex = 3  
          '  
          'borderWidthLabel  
          '  
          Me.borderWidthLabel.AutoSize = True  
          Me.borderWidthLabel.Location = New System.Drawing.Point(20, 79)  
          Me.borderWidthLabel.Margin = New System.Windows.Forms.Padding(10)  
          Me.borderWidthLabel.Name = "borderWidthLabel"  
          Me.borderWidthLabel.Size = New System.Drawing.Size(180, 32)  
          Me.borderWidthLabel.TabIndex = 2  
          Me.borderWidthLabel.Text = "Border Width"  
          '  
          'saveButton  
          '  
          Me.saveButton.DialogResult = System.Windows.Forms.DialogResult.OK  
          Me.saveButton.Location = New System.Drawing.Point(609, 13)  
          Me.saveButton.Name = "saveButton"  
          Me.saveButton.Size = New System.Drawing.Size(160, 70)  
          Me.saveButton.TabIndex = 0  
          Me.saveButton.Text = "Save"  
          '  
          'rejectButton  
          '  
          Me.rejectButton.DialogResult = System.Windows.Forms.DialogResult.Cancel  
          Me.rejectButton.Location = New System.Drawing.Point(443, 13)  
          Me.rejectButton.Name = "rejectButton"  
          Me.rejectButton.Size = New System.Drawing.Size(160, 70)  
          Me.rejectButton.TabIndex = 1  
          Me.rejectButton.Text = "Cancel"  
          '  
          'tableLayoutPanel1  
          '  
          Me.tableLayoutPanel1.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle())  
          Me.tableLayoutPanel1.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!))  
          Me.tableLayoutPanel1.Controls.Add(Me.borderColorLabel, 0, 0)  
          Me.tableLayoutPanel1.Controls.Add(Me.borderColorComboBox, 1, 0)  
          Me.tableLayoutPanel1.Controls.Add(Me.borderWidthLabel, 0, 1)  
          Me.tableLayoutPanel1.Controls.Add(Me.borderWidthNumericUpDown, 1, 1)  
          Me.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill  
          Me.tableLayoutPanel1.Location = New System.Drawing.Point(0, 0)  
          Me.tableLayoutPanel1.Name = "tableLayoutPanel1"  
          Me.tableLayoutPanel1.Padding = New System.Windows.Forms.Padding(10)  
          Me.tableLayoutPanel1.RowStyles.Add(New System.Windows.Forms.RowStyle())  
          Me.tableLayoutPanel1.RowStyles.Add(New System.Windows.Forms.RowStyle())  
          Me.tableLayoutPanel1.Size = New System.Drawing.Size(792, 170)  
          Me.tableLayoutPanel1.TabIndex = 0  
          '  
          'flowLayoutPanel1  
          '  
          Me.flowLayoutPanel1.AutoSize = True  
          Me.flowLayoutPanel1.Controls.Add(Me.saveButton)  
          Me.flowLayoutPanel1.Controls.Add(Me.rejectButton)  
          Me.flowLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Bottom  
          Me.flowLayoutPanel1.FlowDirection = System.Windows.Forms.FlowDirection.RightToLeft  
          Me.flowLayoutPanel1.Location = New System.Drawing.Point(0, 170)  
          Me.flowLayoutPanel1.Name = "flowLayoutPanel1"  
          Me.flowLayoutPanel1.Padding = New System.Windows.Forms.Padding(10)  
          Me.flowLayoutPanel1.Size = New System.Drawing.Size(792, 96)  
          Me.flowLayoutPanel1.TabIndex = 1  
          '  
          'BorderEditorForm  
          '  
          Me.AcceptButton = Me.saveButton  
          Me.ClientSize = New System.Drawing.Size(792, 266)  
          Me.Controls.Add(Me.tableLayoutPanel1)  
          Me.Controls.Add(Me.flowLayoutPanel1)  
          Me.MaximizeBox = False  
          Me.MinimizeBox = False  
          Me.Name = "BorderEditorForm"  
          Me.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent  
          Me.Text = "Edit Border"  
          CType(Me.borderWidthNumericUpDown, System.ComponentModel.ISupportInitialize).EndInit()  
          Me.tableLayoutPanel1.ResumeLayout(False)  
          Me.tableLayoutPanel1.PerformLayout()  
          Me.flowLayoutPanel1.ResumeLayout(False)  
          Me.ResumeLayout(False)  
          Me.PerformLayout()    
      End Sub  
      Public Sub New()  
          MyBase.New()  
          InitializeComponent()  
          borderColorComboBox.DataSource = GetType(Color).GetProperties() _  
              .Where(Function(x) x.PropertyType = GetType(Color)) _  
              .Select(Function(x) x.GetValue(Nothing)).ToList()  
      End Sub  
      Private Sub borderColorComboBox_DrawItem(sender As Object, e As DrawItemEventArgs) Handles borderColorComboBox.DrawItem  
          e.DrawBackground()  
          If e.Index >= 0 Then  
      
              Dim txt = borderColorComboBox.GetItemText(borderColorComboBox.Items(e.Index))  
              Dim Color = CType(borderColorComboBox.Items(e.Index), Color)  
              Dim r1 = New Rectangle(e.Bounds.Left + 1, e.Bounds.Top + 1,  
              2 * (e.Bounds.Height - 2), e.Bounds.Height - 2)  
              Dim r2 = Rectangle.FromLTRB(r1.Right + 2, e.Bounds.Top,  
              e.Bounds.Right, e.Bounds.Bottom)  
              Using b As New SolidBrush(Color)  
                  e.Graphics.FillRectangle(b, r1)  
              End Using  
              e.Graphics.DrawRectangle(Pens.Black, r1)  
              TextRenderer.DrawText(e.Graphics, txt, borderColorComboBox.Font, r2,  
              borderColorComboBox.ForeColor, TextFormatFlags.Left + TextFormatFlags.VerticalCenter)  
          End If  
      End Sub  
      
      End Class
    5. Add a class called BorderEditor.vb with the content: Imports System.ComponentModel
      Imports System.Drawing.Design
      Imports System.Windows.Forms.Design
      Public Class BorderEditor
      Inherits UITypeEditor
      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
      If (value Is Nothing OrElse value.GetType() IsNot GetType(Border)) Then
      Throw New InvalidOperationException()
      End If
      Dim editorService = CType(provider.GetService(GetType(IWindowsFormsEditorService)), IWindowsFormsEditorService)
      If (editorService IsNot Nothing) Then
      Dim f = New BorderEditorForm
      f.Border = CType(value, Border)
      If (editorService.ShowDialog(f) = DialogResult.OK) Then
      Return f.Border
      End If
      End If
      Return value
      End Function
      End Class
    6. Add a class called MyControl.vb with the following content: Imports System.ComponentModel
      Imports System.Drawing.Design
      Public Class MyControl
      Inherits Control
      Private m_border As Border = New Border With {.BorderColor = Color.Blue, .BorderWidth = 5}  
      <Editor(GetType(BorderEditor), GetType(UITypeEditor))>  
      Public Property Border() As Border  
          Get  
              Return m_border  
          End Get  
          Set(ByVal value As Border)  
              m_border = value  
              Me.Invalidate()  
          End Set  
      End Property  
      Protected Overrides Sub OnPaint(e As PaintEventArgs)  
          MyBase.OnPaint(e)  
          If (m_border IsNot Nothing) Then  
              Using p As New Pen(Border.BorderColor, Border.BorderWidth)  
                  p.Alignment = Drawing2D.PenAlignment.Inset  
                  e.Graphics.DrawRectangle(p, Me.ClientRectangle)  
              End Using  
          End If  
      End Sub  
      
      End Class
    7. Rebuild the project.
    8. Add an instance of MyControl to your form
    9. In the property editor, find Border property and click on [...] in front of it and it shows the modal. You can then save the property and see the appearance of your control changes.

    If it's possible to make a property that takes 1 or more required parameters

    Yes, it is possible to add any kind of validation in the property setter and if the property value if not valid, then throw an exception, for example:

    Public Property FirstName() As String  
        Get  
            Return m_firstName  
        End Get  
        Set(ByVal value As String)  
            If (String.IsNullOrEmpty(value)) Then  
                Throw New Exception("FirstName is mandatory.")  
            End If  
            m_firstName = value  
        End Set  
    End Property  
    

    Whether it's possible to tweak the property-page type that handles "collection" properties

    Yes it's possible. In above example you learned how to edit any kind of object. A collection is also a type, so you can easily create a custom UITypeEditor for that and show a DataGridView or any other UI. A good base class for this kind of properties is CollectionEditor. You can create a custom editor form, from scratch or by deriving from CollectionEditorForm.

    2 people found this answer helpful.

0 additional answers

Sort by: Most helpful