Drawing a WPF UserControl with DataBinding to an Image
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:
- Make sure your control has been measured and arranged.
- If your control uses Loaded events, make sure you are attached to a PresentationSource.
- 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.
Well, one of your problems is that you need to call Measure and Arrange on your UserControl before trying to render. Put this before creating the RenderTargetBitmap object:
template.Measure(new Size(template.Width, template.Height));
template.Arrange(new Rect(new Size(template.Width, template.Height)));
That will at least get your UserControl to start rendering.
The second problem is the data binding. I haven't been able to crack that one; there may be something extra you need to do to get the bindings to evaluate. However, you can work around it: If you set the TextBlock contents directly rather than through data binding, it does work.