Search code examples
wpfimagedata-binding.net-3.5

Drawing a WPF UserControl with DataBinding to an Image


So I'm trying to use a WPF User Control to generate a ton of images from a dataset where each item in the dataset would produce an image...

I'm hoping I can set it up in such a way that I can use WPF databinding, and for each item in the dataset, create an instance of my user control, set the dependency property that corresponds to my data item, and then draw the user control to an image, but I'm having problems getting it all working (not sure whether databinding or drawing to the image is my problem)

Sorry for the massive code dump, but I've been trying to get this working for a couple of hours now, and WPF just doesn't like me (have to learn at some point though...)

My User Control looks like this:

<UserControl x:Class="Bleargh.ImageTemplate"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:c="clr-namespace:Bleargh"
    x:Name="ImageTemplateContainer"
    Height="300" Width="300">

    <Canvas>
        <TextBlock Canvas.Left="50" Canvas.Top="50"  Width="200" Height="25" FontSize="16" FontFamily="Calibri" Text="{Binding Path=Booking.Customer,   ElementName=ImageTemplateContainer}" />
        <TextBlock Canvas.Left="50" Canvas.Top="100" Width="200" Height="25" FontSize="16" FontFamily="Calibri" Text="{Binding Path=Booking.Location,   ElementName=ImageTemplateContainer}" />
        <TextBlock Canvas.Left="50" Canvas.Top="150" Width="200" Height="25" FontSize="16" FontFamily="Calibri" Text="{Binding Path=Booking.ItemNumber, ElementName=ImageTemplateContainer}" />
        <TextBlock Canvas.Left="50" Canvas.Top="200" Width="200" Height="25" FontSize="16" FontFamily="Calibri" Text="{Binding Path=Booking.Description,ElementName=ImageTemplateContainer}" />
    </Canvas>

</UserControl>

And I've added a dependency property of type "Booking" to my user control that I'm hoping will be the source for the databound values:

public partial class ImageTemplate : UserControl
{
    public static readonly DependencyProperty BookingProperty = DependencyProperty.Register("Booking", typeof(Booking), typeof(ImageTemplate));
    public Booking Booking
    {
        get { return (Booking)GetValue(BookingProperty); }
        set { SetValue(BookingProperty, value); }
    }

    public ImageTemplate()
    {
        InitializeComponent();
    }
}

And I'm using the following code to render the control:

List<Booking> bookings = Booking.GetSome();
for(int i = 0; i < bookings.Count; i++)
{
    ImageTemplate template = new ImageTemplate();
    template.Booking = bookings[i];

    RenderTargetBitmap bitmap = new RenderTargetBitmap(
        (int)template.Width,
        (int)template.Height,
        120.0,
        120.0,
        PixelFormats.Pbgra32);

    bitmap.Render(template);

    BitmapEncoder encoder = new PngBitmapEncoder();
    encoder.Frames.Add(BitmapFrame.Create(bitmap));

    using (Stream s = File.OpenWrite(@"C:\Code\Bleargh\RawImages\" + i.ToString() + ".png"))
    {
        encoder.Save(s);
    }
}

EDIT:

I should add that the process works without any errors whatsoever, but I end up with a directory full of plain-white images, not text or anything... And I have confirmed using the debugger that my Booking objects are being filled with the proper data...

EDIT 2:

Did something I should have done a long time ago, set a background on my canvas, but that didn't change the output image at all, so my problem is most definitely somehow to do with my drawing code (although there may be something wrong with my databinding too)


Solution

  • RenderTargetBitmap renders the current state of your control. In your case your control has not initialized so it still appears white.

    To get your code to initialize properly before Render() you need to do three things:

    1. Make sure your control has been measured and arranged.
    2. If your control uses Loaded events, make sure you are attached to a PresentationSource.
    3. Make sure all DispatcherPriority.Render and above events have completed.

    If you do these three things your RenderTargetBitmap will come out identically to the way the control appears when you add it to a Window.

    Forcing a Measure/Arrange on your control

    This is as simple as:

    template.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
    template.Arrange(new Rect(template.DesiredSize));
    

    This code forces measure/arrange. It is simplest to pass in double.PositiveInfinity for the width and height because it allows your UserControl to choose its own Width and Height. If you explicitly set the width/height it doesn't matter much, but this way your UserControl has the option of using WPF's layout system to automatically grow when necessary if the data is larger than expected. By the same token it is better to use template.DesiredSize for the Arrange rather than passing in a specific size.

    Attaching a PresentationSource

    This is only necessary if your control or elements within your control rely on the Loaded event.

    using(var source = new HwndSource(new HwndSourceParameters())
                           { RootVisual = template })
    {
      ...
    }
    

    When the HwndSource is created the visual tree of the template is notified that it has been "Loaded". The "using" block makes sure the template is "Unloaded" at the end of the "using" statement (last closing curly brace). An alternative to a using() statement would be to use GC.KeepAlive:

    GC.KeepAlive(new HwndSource(...) { ... });
    

    Flushing the Dispatcher queue down to DispatcherPriority.Render

    Just use Dispatcher.Invoke:

    Dispatcher.Invoke(DispatcherPriority.Loaded, new Action(() => {}));
    

    This causes an empty action to be invoked after all Render and higher priority actions have completed. The Dispatcher.Invoke method processes the dispatcher queue until it is empty down to Loaded level (which is right below Render).

    The reason this is necessary is that many WPF UI components use the Dispatcher queue to delay processing until the control is ready to render. This significantly cuts down on unnecessary re-computation of visual properties during binding and other operations.

    Where to add this code

    Add all three of these steps after you set your data context (template.Booking = ...) and before you call RenderTargetBitmap.Render.

    Additional suggestions

    There is a much easier way to make your binding work. In code, just set the booking as a DataContext. This removes the need to use ElementName and the Booking property:

    foreach(var booking in Booking.GetSome())
    {
      var template = new ImageTemplate { DataContext = booking };
    
      ... code from above ...
      ... RenderTargetBitmap code ...
    }
    

    By using the DataContext, the TextBox binding is greatly simplified:

    <UserControl ...>
      <Canvas>
        <TextBlock ... Text="{Binding Customer}" />
        <TextBlock ... Text="{Binding Location}" />
        <TextBlock ... Text="{Binding ItemNumber}" />
        <TextBlock ... Text="{Binding Description}" />
    

    If you have a particular reason for using the Booking DependencyProperty you can still simplify your bindings by setting the DataContext at the <UserControl> level rather than using ElementName:

    <UserControl ...
      DataContext="{Binding Booking, RelativeSource={RelativeSource Self}}">
      <Canvas>
        <TextBlock ... Text="{Binding Customer}" />
    

    I would also recommend you use a StackPanel instead of a Canvas for this purpose, and you should also consider using a style to set the font, text size and spacing:

    <UserControl ...
      Width="300" Height="300">
    
      <UserControl.Resources>
        <Style TargetType="TextBlock">
          <Setter Property="FontSize" Value="16" />
          <Setter Property="FontFamily" Value="Calibri" />
          <Setter Property="Height" Value="25" />
          <Setter Property="Margin" Value="50 25 50 0" />
        </Style>
      </UserControl.Resources>
    
      <StackPanel>
        <TextBlock Text="{Binding Customer}" />
        <TextBlock Text="{Binding Location}" />
        <TextBlock Text="{Binding ItemNumber}" />
        <TextBlock Text="{Binding Description}" />
      </StackPanel>
    </UserControl>
    

    Note that all the layout is done by WPF's layout given the UserControl size and the specified height and margin. Also note that the TextBlock only needs to specify the Text -- everything else is handled by the style.