Load user control programmatically using LoadControl(Type, Object())
Asked Answered
C

3

8

I’m adding web user controls to a page dynamically. Using the LoadControl method that only takes a virtual path pointing to the .ascx works pretty nicely. However, the overload of LoadControl that takes a type and an array of parameters is causing me some headaches.

The web user control is instantiated as expected, but the controls contained within the web user control are null and I get an exception as soon as I try to work with them. Strange, because it’s working when use the first version of LoadControl.

The web user control, simple, with a Literal control:

<%@ Control Language="vb" AutoEventWireup="false" CodeBehind="MyControl.ascx.vb" Inherits="MyControl" %>
<asp:Literal ID="myLiteral" runat="server"></asp:Literal>

The controls' code behind:

Public Class MyControl
  Inherits System.Web.UI.UserControl

  Public Property Data As MyData  

  Public Sub New()

  End Sub

  Public Sub New(data As MyData)
    Me.Data = data
  End Sub

  Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
    myLiteral.Text = Data.ID ' The Literal is null, but ONLY when I use the second LoadControl() method!
  End Sub

End Class

And the relevant code from the .aspx from which I'm trying to dynamically load the control:

Private Sub Page_Init(sender As Object, e As System.EventArgs) Handles Me.Init
  Dim x = LoadControl(GetType(MyControl), New Object() {New MyData With {.ID = 117}})
  Page.Controls.Add(x)

  ' Using LoadControl("MyControl.ascx") works as expected!
End Sub
Cobby answered 25/2, 2012 at 17:37 Comment(0)
C
2

With a bit of help from this article by Steven Robbins, I ended up with a very convenient extension method instead:

Imports System.Runtime.CompilerServices
Imports System.Web.UI
Imports System.Reflection

Module LoadControls
  <Extension()> _
  Public Function LoadControl(templateControl As TemplateControl, virtualPath As String, ParamArray constructorParams() As Object) As UserControl
    Dim control = TryCast(templateControl.LoadControl(virtualPath), UserControl)
    Dim paramTypes = constructorParams.Select(Function(p) p.GetType()).ToArray
    Dim constructor = control.GetType().BaseType.GetConstructor(paramTypes)

    If constructor Is Nothing Then ' Nothing if no such constructor was found.
      Throw New ArgumentException(String.Format("No constructor for control '{0}' with {1} parameter(s) were found.", virtualPath, paramTypes.Count))
    Else
      constructor.Invoke(control, constructorParams)
    End If

    Return control
  End Function

End Module
Cobby answered 26/2, 2012 at 8:29 Comment(3)
That looks like you are invoking a constructor on an already created object -- how does that work?Insistent
@Insistent Since constructors are just static methods with some extra hubbub, it just works.Ridenhour
Here's a quick demo: ideone.com/IoqU2Z (the code fails on most online editors because it requires some security demand, but it runs with full trust).Ridenhour
L
3

Per this post I found: http://forums.asp.net/t/1375955.aspx, it was said that just not use it.

A page that loads a user control using the Page.LoadControl(Type, Object[]) does not seem to create its children added in the ascx file. Using Page.LoadControl(String) works as expected.

My understanding is that based on the code behind stuff, the ascx is a child class that Inherits MyControl but not MyControl itself, you need to understand the ascx is not a definition of MyControl but it's and extension, so when you try use type name to create the control, you are creating a parent control but not the one you want.

For prove this, just define a private property in MyControl, and try to bind the value on the ascx, you will then get an error as the child class can't access any private thing in it's base class.

Locke answered 25/2, 2012 at 18:48 Comment(0)
C
2

With a bit of help from this article by Steven Robbins, I ended up with a very convenient extension method instead:

Imports System.Runtime.CompilerServices
Imports System.Web.UI
Imports System.Reflection

Module LoadControls
  <Extension()> _
  Public Function LoadControl(templateControl As TemplateControl, virtualPath As String, ParamArray constructorParams() As Object) As UserControl
    Dim control = TryCast(templateControl.LoadControl(virtualPath), UserControl)
    Dim paramTypes = constructorParams.Select(Function(p) p.GetType()).ToArray
    Dim constructor = control.GetType().BaseType.GetConstructor(paramTypes)

    If constructor Is Nothing Then ' Nothing if no such constructor was found.
      Throw New ArgumentException(String.Format("No constructor for control '{0}' with {1} parameter(s) were found.", virtualPath, paramTypes.Count))
    Else
      constructor.Invoke(control, constructorParams)
    End If

    Return control
  End Function

End Module
Cobby answered 26/2, 2012 at 8:29 Comment(3)
That looks like you are invoking a constructor on an already created object -- how does that work?Insistent
@Insistent Since constructors are just static methods with some extra hubbub, it just works.Ridenhour
Here's a quick demo: ideone.com/IoqU2Z (the code fails on most online editors because it requires some security demand, but it runs with full trust).Ridenhour
F
0

Jakob, thank you so much for the extension function. It has come in very handy. However it doesn't account for constructors that take parameters ByRef.

I'm sure the following modification can be written much shorter and avoid rewriting the GetConstructor logic but this is what I came up with to handle ByRef parameters in the constructor.

I tried to keep it generic so that setting of the parameters to ByRef is based on a matching constructor instead of hard coded to a parameter index.

Edit: There is a downside to this function. The constructors of your user control get called twice. Once by the first LoadControl and then again when the second constructor is found and given parameters. Be aware that this has Page_Load also run twice. I encapsulated the actual page_load logic in a sub and has the second constructor call it, avoiding the issue.

Imports System.Runtime.CompilerServices
Imports System.Web.UI
Imports System.Reflection

<Extension()> Public Function LoadControl(templateControl As TemplateControl, virtualPath As String, ParamArray constructorParams() As Object) As UserControl
    Dim control As UserControl = TryCast(templateControl.LoadControl(virtualPath), UserControl)
    Dim paramTypes() As Type = constructorParams.Select(Function(p) p.GetType()).ToArray
    Dim isMatch As Boolean = True

    ' ByRef Parameters
    For Each cnst As ConstructorInfo In control.GetType.BaseType.GetConstructors
        If cnst.GetParameters.Count = paramTypes.Count Then
            Dim tempTypes(paramTypes.Count - 1) As Type
            isMatch = True
            Array.Copy(paramTypes, tempTypes, paramTypes.Length)

            For i As Integer = 0 To paramTypes.Count - 1
                If cnst.GetParameters(i).ParameterType.FullName.TrimEnd("&") = paramTypes(i).FullName Then
                    If cnst.GetParameters(i).ParameterType.IsByRef Then tempTypes(i) = paramTypes(i).MakeByRefType Else tempTypes(i) = paramTypes(i)
                Else
                    isMatch = False
                End If
            Next

            If isMatch Then
                cnst.Invoke(control, constructorParams)
                Exit For
            End If
        End If
    Next

    If not isMatch Then
        Throw New ArgumentException(String.Format("No constructor for control '{0}' with {1} parameter(s) were found.", control, paramTypes.Count))
    End If

    Return control
End Function
Footpound answered 6/5, 2013 at 16:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.