Search code examples
wpfvb.netuser-controlswpf-controlsinstance

VB.NET WPF - How to access an instance of a user control


I am making a WPF application for quoting, and in one of my windows (defined as a user control, not a window), I have a print button that successfully renders the window to a bitmap and then into an image and goes into a print preview screen. I coded this all, but now I have to print other windows in the print page.

So I wrote this function that works for the current window

Private Function RenderWindowToBitmap(userControl As UserControl, scaleFactor As Double) As System.Drawing.Image
    Dim parentWindow As Window = Window.GetWindow(userControl)

    If parentWindow Is Nothing Then
        Throw New InvalidOperationException("Unable to retrieve the parent window of the specified user control.")
    End If

    Dim rtb As New RenderTargetBitmap(CInt(parentWindow.ActualWidth * scaleFactor), CInt(parentWindow.ActualHeight * scaleFactor), 96, 96, PixelFormats.Pbgra32)
    Dim visual As New DrawingVisual()

    Using dc As DrawingContext = visual.RenderOpen()
        dc.PushTransform(New ScaleTransform(scaleFactor, scaleFactor))
        dc.DrawRectangle(New VisualBrush(parentWindow), Nothing, New Rect(New Size(parentWindow.ActualWidth, parentWindow.ActualHeight)))
    End Using

    rtb.Render(visual)

    Dim png As New PngBitmapEncoder()
    png.Frames.Add(BitmapFrame.Create(rtb))
    Dim stream As New MemoryStream()
    png.Save(stream)

    Return System.Drawing.Image.FromStream(stream)
End Function

And when it's called for the current window it works perfectly and looks like this bitmaps.Add(RenderWindowToBitmap(Me, scaleFactor))

But now I want to throw other user controls into this function. My print function exists in this user control's xaml.vb file (called General.xaml.vb), so I just need the instances of the other user controls to get this print to work. I have a main view model that is passed in the constructor for this page, and the viewmodels are required to make a new instance of its respective User Control.

So here is my RollEnd.xaml.vb class that is one of the user controls I need:

Partial Public Class RollEnd
    Inherits UserControl

    Public Sub New(viewModel As RollEndViewModel)
        InitializeComponent()
        Me.DataContext = viewModel
    End Sub

    'Private Sub ComboBox_Loaded(sender As Object, e As RoutedEventArgs)
    '    Dim comboBox As ComboBox = CType(sender, ComboBox)
    '    Dim dataContext = comboBox.DataContext
    'End Sub
End Class

My General.xaml.vb constructor

 Public Sub New(viewModel As MainViewModel)
     InitializeComponent()
     _mainViewModel = viewModel
     _quoteViewModel = viewModel.QuoteViewModel
     Me.DataContext = _quoteViewModel
 End Sub

As you can see, my General UserControl has the mainViewModel passed in which contains the RollEndViewModel, so I don't understand why when I run my code, the instances of the other User Controls are null.

I tried making the UserControls properties of my MainViewModel, and I was hoping that by instantiating them in the constructor of my mainViewModel, they would be filled on load. But nope they were null. I tried traversing the Visual Tree of User Controls with this function but it just returns nothing

Public Function FindUserControl(Of T As UserControl)(parent As DependencyObject) As T
    If parent Is Nothing Then Return Nothing

    For i As Integer = 0 To VisualTreeHelper.GetChildrenCount(parent) - 1
        Dim child As DependencyObject = VisualTreeHelper.GetChild(parent, i)

        If TypeOf child Is T Then
            Return CType(child, T)
        End If

        Dim result As T = FindUserControl(Of T)(child)
        If result IsNot Nothing Then
            Return result
        End If
    Next

    Return Nothing
End Function

I also tried making the User Controls properties in my MainWindow.xaml.vb class like this

This line below is in the constructor for the main window _rollEndControl = New RollEnd(MainViewModel.Instance.RollEndViewModel)

_rollEndControl is a property with this getter in my MainWindow.xaml.vb:

Public Function GetRollEndControl() As RollEnd
    Return _rollEndControl
End Function

and when I call this function in General.xaml.vb it returns nothing.

Sorry this seems like a complex issue, but I am hoping if anyone here knows a simple way to just get access to an instance of the userControl: thanks!

Also for reference my MainViewModel is a singleton Lazy initialization.


Solution

  • I ended up solving this problem by making all the user controls implement an interface I created called IPrintable.

    So now my code looks like this

    Partial Public Class RollEnd
    Inherits UserControl
    Implements IPrintable
    Public ReadOnly Property IPrintable_Loaded As Task(Of UserControl) Implements IPrintable.Loaded
    Get
        Dim tcs As New TaskCompletionSource(Of UserControl)()
    
        Dim handler As RoutedEventHandler = Nothing
    
        handler = Sub(sender, e)
                      RemoveHandler Me.Loaded, handler
                      tcs.SetResult(Me)
                  End Sub
    
        AddHandler Me.Loaded, handler
        Return tcs.Task
    End Get
    

    End Property End Class

    I also created the interface that looks like this

    Public Interface IPrintable
    
    ReadOnly Property Loaded As Task(Of UserControl)
    

    End Interface

    So I did it this way so the pages would render for printing in my application. Really the instances of the user controls were null because they literally weren't rendered. Really the only code that was hard to understand was the IPrintable_Loaded property that essentially is converting an event into a task. The reason for this is so we can use async. That way the pages in my application can all load smoothly.

    The actual implementation for anyone interested for my case looks like this:

    Dim bitmaps As New List(Of System.Drawing.Image)
    General_Click(Nothing, Nothing)
    Dim UC As IPrintable = MainContent.Content
    Await UC.Loaded
    bitmaps.Add(RenderWindowToBitmap(MainContent.Content, scaleFactor))
    

    Create a list of pages I want to print (bitmaps), call a method that literally just makes the MainContent.Content as the "General" user control (I have 4 different user controls), and then I create a variable UC (user control) that is IPrintable that is set to the mainContent and then we await for that task to finish and render the window. I repeat this code 4 times for all my user controls.

    I doubt anyone would have this specific problem, but if you ever need to access an instance of a user control in WPF, using an interface of some kind might help you do it!