Search code examples
c#wpfuser-controlspngdatacontext

Data Context not updating elements when exporting control to a png WPF C#


I am working on a program that can print out cards for a card game. In the program there is a class and a user control for each card type. I have a big list containing all of my cards. To print the cards, I dynamically create the controls in the code behind, add those to a Print Document, and have it print. This works! Here is an example of the generation of the control that I use:

AttackCardControl cpTop = new AttackCardControl();
cpTop.DataContext = StateManager.CardsToPrint.ElementAt(i);

Viewbox vb = new Viewbox() { Width = 240, Height = 336 };
vb.Child = cpTop;

sp.Children.Add(vb);

sp is a Stack Panel I use to arrange the cards on a page, and i is the iterator for the for loop that this is contained in. I run this through a series of for loops along with some other small irrelevant things and it works just fine.

Now I am creating a new feature that allows a user to export the cards to a PNG. I decided to do it in a very similar way to how I print the cards. This is what I use:

for (int i = 0; i < StateManager.CardsToPrint.Count; ++i)
{
    Canvas cv = new Canvas();
    cv.Width = 825;
    cv.Height = 1125;

    if (StateManager.CardsToPrint.ElementAt(i).GetType() == typeof(AttackCard))
    {
        AttackCardControl cardControl = new AttackCardControl();
        cardControl.DataContext = StateManager.CardsToPrint.ElementAt(i);
        cv.Children.Add(cardControl);
    }

    FileHandling.ExportToPng(new Uri(path + "/" + StateManager.CardsToPrint.ElementAt(i).Name + ".png"), cv);
}

This should export all of my Attack cards in the list to a png, and it does. However the elements tied to the Data Context are not getting updated (The Name, Effect Text, Flavor Text, etc.). So at the end, I just get a blank Attack Card that has a proper file name. When I follow it through when debugging, the DataContext remains populated with all the correct data, but when it exports to a png, none of the elements of the user control show their values. Here is the code to my ExportToPng method:

public static void ExportToPng(Uri path, Canvas surface)
{
    if (path == null) return;

    // Save current canvas transform
    Transform transform = surface.LayoutTransform;
    // reset current transform (in case it is scaled or rotated)
    surface.LayoutTransform = null;

    // Get the size of canvas
    System.Windows.Size size = new System.Windows.Size(surface.Width, surface.Height);
    // Measure and arrange the surface
    // VERY IMPORTANT
    surface.Measure(size);
    surface.Arrange(new Rect(size));

    // Create a render bitmap and push the surface to it
    RenderTargetBitmap renderBitmap =
        new RenderTargetBitmap(
        (int)size.Width,
        (int)size.Height,
        96d,
        96d,
        PixelFormats.Pbgra32);
    renderBitmap.Render(surface);

    // Create a file stream for saving image
    using (FileStream outStream = new FileStream(path.LocalPath, FileMode.Create))
    {
        // Use png encoder for our data
        PngBitmapEncoder encoder = new PngBitmapEncoder();
        // push the rendered bitmap to it
        encoder.Frames.Add(BitmapFrame.Create(renderBitmap));
        // save the data to the stream
        encoder.Save(outStream);
    }

    // Restore previously saved layout
    surface.LayoutTransform = transform;
}

I just don't understand why it works for printing, but it doesn't work for this exporting to a PNG. Any help would be greatly appreciated.

UPDATE

I just created a sample project that has just this issue, and can not seem to find a way to resolve it. The sample project has 3 things: MainWindow, UserControl1, and Model.cs.

The MainWindow is what I use for controls and to display the issue. The code behind contains all the logic for where the error occurs. I did not bother with MVVM for this simple example. Here is the XAML:

<Window x:Class="ExportTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ExportTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel Name="sp" Margin="10">
        <Button Content="Export Control to PNG" Click="Button_Click" Width="150" Height="30"/>
    </StackPanel>
</Window>

And here is the code behind:

using System;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace ExportTest
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            Model m = new Model("Testing");
            UserControl1 uc1 = new UserControl1();
            uc1.DataContext = m;
            sp.Children.Add(uc1);
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var dialog = new System.Windows.Forms.FolderBrowserDialog();
            if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
            {
                string path = dialog.SelectedPath;
                Canvas cv = new Canvas();
                cv.Width = 825;
                cv.Height = 1125;

                Model m = new Model("Testing");
                UserControl1 uc1 = new UserControl1();
                uc1.DataContext = m;

                cv.Children.Add(uc1);

                // The card control is losing it's data context
                ExportToPng(new Uri(path + "/" + m.Name + ".png"), cv);
            }

        }

        public void ExportToPng(Uri path, Canvas surface)
        {
            if (path == null) return;

            //// Save current canvas transform
            //Transform transform = surface.LayoutTransform;
            //// reset current transform (in case it is scaled or rotated)
            //surface.LayoutTransform = null;

            // Get the size of canvas
            Size size = new Size(surface.Width, surface.Height);
            // Measure and arrange the surface
            // VERY IMPORTANT
            surface.Measure(size);
            surface.Arrange(new Rect(size));

            // Create a render bitmap and push the surface to it
            RenderTargetBitmap renderBitmap =
              new RenderTargetBitmap(
                (int)size.Width,
                (int)size.Height,
                96d,
                96d,
                PixelFormats.Pbgra32);
            renderBitmap.Render(surface);

            // Create a file stream for saving image
            using (FileStream outStream = new FileStream(path.LocalPath, FileMode.Create))
            {
                // Use png encoder for our data
                PngBitmapEncoder encoder = new PngBitmapEncoder();
                // push the rendered bitmap to it
                encoder.Frames.Add(BitmapFrame.Create(renderBitmap));
                // save the data to the stream
                encoder.Save(outStream);
            }

            //// Restore previously saved layout
            //surface.LayoutTransform = transform;
        }
    }
}

Here is the XAML for the UserControl1 (the user control that we are trying to export to a PNG):

<UserControl x:Class="ExportTest.UserControl1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:ExportTest"
             mc:Ignorable="d">
    <StackPanel Orientation="Horizontal" Background="Red">
        <TextBlock Text="Name: "/>
        <TextBlock Text="{Binding Name}"/>
    </StackPanel>
</UserControl>

There is no code behind needed for this user control.

Lastly, we have the Model calss. This will be the model for the Data Context for UserControl1. Here is the class:

namespace ExportTest
{
    public class Model
    {
        private string _Name;

        public string Name { get { return _Name; } set { _Name = value; } }

        public Model()
        {
            Name = "";
        }

        public Model(string name)
        {
            Name = name;
        }
    }
}

This will recreate my exact issue. In the Main Window, we get something that looks like this:

As you can see, the UserControl1 is catching the DataContext and is displaying the name Testing. If I click the Export Control to PNG button though, this is what I get for my exported PNG file:

Somehow, my data is missing. Any clues? Thanks for the help!


Solution

  • So I just found the answer here: Saving FrameworkElement with its DataContext to image file does no succeed

    There needs to be a call to UpdateLayout() to fix the bindings right before we render the control as an image.