alex
alex

Reputation: 331

How to pass an Attribute through an interface implementation on .Net

I'm working in a winform on .Net who generates controls automatically based on a model Class using the System.Reflection like this:

Imports System.Reflection

Public Class DynamicForm(Of Model As {IModelBase, Class, New})
    Protected Sub InitializeComponent()
        'A lot of not relevant code here...
        For Each prop As PropertyInfo In GetType(Model).GetProperties
            If Not HasControl(prop) Then Continue For
            Dim control = CreateControl(prop)
            Dim label = CreateLabel(prop)
            SetLocation(label, control)
            Me.Controls.Add(control)
            Me.Controls.Add(label)
            ResizeWindow(control.Height)
        Next
    End Sub
End Class

Of course a model can have a lot of properties who I don't want to be edited directly by the user so I go with the solution to use attributes to mark which properties you want to display like this:

Public Class HasControl
    Inherits Attribute

    Public Property Label As String

    Public Sub New(label As String)
        Me.Label = label
    End Sub
End Class

And then my model look like:

Public Class Clients
    Implements IModelBase

    <HasControl("First name:")>
    Public Property FirstName As String

    Public Property CreatedTime As Date
End Class

Now, I was using the IModelBase interface just only for ensure no any class can be used in this windows but I recently start to use for something else. Many of my models are like Clients, Workers, Sellers, Users, etc. Those models has many similar properties so I make an interface with this properties:

Public Interface IHumanData
    Inherits IModelBase

    <HasControl("First name:")>
    Public Property FirstName As String

    <HasControl("Last name:")>
    Public Property LastName As String
End Interface

Public Class Seller
    Implements IHumanData

    Public Property FirstName As String Implements IHumanData.FirstName

    Public Property LastName As String Implements IHumanData.LastName
End Class

The problem with this is the "HasControl" attribute is not assigned to my Seller's properties. I know if I use inheritance instead of interface implementation this actually works, but inheritance has a limitation when I want to create something like this:

Public Class Worker
    Implements IHumanData, IDateStorageData, ILocationData, ISortOfRandomData

    ' A bunch of properties auto generated by VisualStudio
End Class

So, there is actually an easy way to pass an Attribute through an interface implementation?

Upvotes: 0

Views: 456

Answers (1)

pfx
pfx

Reputation: 23214

You should query the interface definitions for the properties implementing the HasControl attribute.
To do so, you retrieve the interfaces implemented by the object instance being used in the form.

The routine to build the form can look like below.

Sub buildForm(model As IModelBase)
    Dim interfaceTypes As Type() = model.GetType().FindInterfaces(New TypeFilter(Function(t, c) True), Nothing)

    For Each interfaceType As Type In interfaceTypes
        Dim properties As PropertyInfo() = interfaceType.GetProperties()
        For Each prop As PropertyInfo In properties
            Dim attributes As IEnumerable(Of HasControlAttribute) = prop.GetCustomAttributes(Of HasControlAttribute)()
            For Each attribute As HasControlAttribute In attributes
                Console.Write(attribute.Name)
                Console.Write(": ")
                Console.WriteLine(CStr(prop.GetValue(model, Nothing)))
                ' Build label and control here ... 
            Next
        Next
    Next
End Sub

You can also retrieve these interfaces from the generic type parameter (in your form Model).

Dim interfaceTypes As Type() = GetType(Model).FindInterfaces(New TypeFilter(Function(t, c) True), Nothing)

Call it by passing it the object instance being used in your form, eg. a Worker instance.

Dim worker As Worker = New Worker()
worker.FirstName = "John"
worker.LastName = "Doe"
worker.City = "Brussels"
worker.Age = 40
buildForm(worker)

Which results into only the marked properties being considered.
Notice that the property Age (as example defined on Worker doesn't appear.

First name: John
Last name: Doe
City: Brussels

IModelBase

Public Interface IModelBase
    ' ...
End Interface

HasControlAttribute

Public Class HasControlAttribute
    Inherits Attribute

    Public Sub New(ByVal name As String)
        Me.Name = name
    End Sub

    Public Property Name As String
End Class

IHumanData

Public Interface IHumanData
    Inherits IModelBase

    <HasControl("First name")>
    Property FirstName As String
    <HasControl("Last name")>
    Property LastName As String
    '... 
End Interface

ILocationData

Public Interface ILocationData
    <HasControl("City")>
    Property City As String
    ' ... 
End Interface

ISortOfRandomData

Public Interface ISortOfRandomData
    '...  
End Interface

Worker

Public Class Worker
    Implements IHumanData, ILocationData

    Public Property FirstName As String Implements IHumanData.FirstName
    Public Property LastName As String Implements IHumanData.LastName
    Public Property City As String Implements ILocationData.City
    Public Property Age As Integer
    ' ...
End Class

UPDATE

As follow up to your comment.

If you want to include the properties defined on the Worker class itself (which are not defined in any of the implemented interfaces) as shown below, then the interfaceTypes array needs to include the Type of the object instance itself.

Dim interfaceTypes As IList(Of Type) = model.GetType().FindInterfaces(New TypeFilter(Function(t, c) True), Nothing).ToList()
interfaceTypes.Add(model.GetType)

Worker with property Age marked with HasControl

Public Class Worker
    Implements IHumanData, ILocationData

    Public Property FirstName As String Implements IHumanData.FirstName
    Public Property LastName As String Implements IHumanData.LastName
    Public Property City As String Implements ILocationData.City

    <HasControl("Age")>
    Public Property Age As Integer
    ' ...
End Class

Upvotes: 1

Related Questions