Есть ли простой/встроенный способ получить точную копию (клон) элемента XAML?

Мне нужно сделать области XAML для печати и поэтому сделать этот обработчик этой кнопки:

private void Button_Click_Print(object sender, RoutedEventArgs e)
{
    Customer.PrintReport(PrintableArea);
}

И в PrintReport я упаковываю элемент framework в другие элементы, чтобы напечатать его немного другим способом, чем на экране, например:

public void PrintReport(FrameworkElement fwe)
{
    StackPanel sp = new StackPanel();
    sp.Children.Add(fwe);
    TextBlock tb = new TextBlock();
    tb.Text = "hello";
    sp.Children.Add(tb);

    PrintDialog dialog = new PrintDialog();
    if (dialog.ShowDialog() == true)
    { 
        dialog.PrintVisual(sp, "Print job"); 
    }
}

Но приведенное выше дает мне следующую ошибку:

Указанный элемент уже логический дочерний элемент другого элемента. Сначала отключите его.

Есть ли простой способ клонировать элемент FrameworkElement, чтобы я мог манипулировать копией, печатать ее, а затем забывать об этом, оставляя исходный элемент в XAML на экране неизменным?

Что-то вроде этого я бы подумал:

FrameworkElement fwe2 = FrameworkElement.Clone(fwe); //pseudo-code

Ответ 1

У меня была аналогичная проблема в моем текущем проекте и она была решена с помощью этого кода.

public static class ExtensionMethods
{
    public static T XamlClone<T>(this T original)
        where T : class
    {
        if (original == null)
            return null;

        object clone;
        using (var stream = new MemoryStream())
        {
            XamlWriter.Save(original, stream);
            stream.Seek(0, SeekOrigin.Begin);
            clone = XamlReader.Load(stream);
        }

        if (clone is T)
            return (T)clone;
        else
            return null;
    }
}

Таким образом, он просто появляется как метод для всех объектов в вашем проекте WPF, вам не нужно давать какие-либо параметры методу и он возвращает объект того же класса, что и оригинал.

Ответ 2

В WPF элементы копирования (или "клонирования" ) почти никогда не верны. Это эффективно делает этот вопрос XY проблемой. То есть вы считаете, что вам нужно буквально клонировать элементы в вашем визуальном дереве. Но вы этого не делаете.

Идиоматический и правильный подход здесь заключается в объявлении DataTemplate, который представляет данные, которые вы хотите распечатать. Конечно, это также означает, что данные, которые вы хотите распечатать, в свою очередь представлены классом модели представления, для которого объявлен DataTemplate (т.е. Через свойство DataType).

Например:

<DataTemplate DataType={x:Type PrintableViewModel}>
  <!-- template contents go here -->
</DataTemplate>

Класс PrintableViewModel - это, конечно, класс модели представления, содержащий данные, которые вы хотите использовать для заполнения визуального дерева, которое будет напечатано.

В XAML для вашего пользовательского интерфейса вы должны использовать его примерно так:

<ContentControl Content={Binding PrintableViewModelProperty}/>

т.е. привяжите свойство Content к свойству в текущем объекте DataContext, который возвращает экземпляр вашего PrintableViewModel, и пусть ContentControl отобразит данные соответствующим образом.

WPF будет искать соответствующий шаблон данных и применять его для отображения в ContentControl. Когда вы хотите распечатать данные, вы просто сделаете что-то вроде этого:

PrintDialog printDialog = new PrintDialog();

if (printDialog.ShowDialog() == true)
{
    ContentControl contentControl = new ContentControl { Content = ((ViewModelClass)DataContext)PrintableViewModelProperty};

    // This part with the margins is not strictly relevant to your question per se,
    // but it useful enough to be worth including here for future reference
    PageImageableArea area = printDialog.PrintQueue.GetPrintCapabilities(printDialog.PrintTicket).PageImageableArea;

    contentControl.Margin = new Thickness(area.OriginWidth, area.OriginHeight,
        printDialog.PrintableAreaWidth - area.ExtentWidth - area.OriginWidth,
        printDialog.PrintableAreaHeight - area.ExtentHeight - area.OriginHeight);

    // This shows retrieving the data template which is declared using the DataType
    // property. Of course, if you simply declare a key and reference it explicitly
    // in XAML, you can just use the key itself here.
    DataTemplateKey key = new DataTemplateKey(typeof(MazeViewModel));

    contentControl.ContentTemplate = (DataTemplate)FindResource(key);
    printDialog.PrintVisual(contentControl, "MazeGenerator");
}

Это заставит WPF автоматически повторно использовать шаблон, который вы уже описали для класса PrintableViewModel, заполнив визуальное поддерево ContentControl в соответствии с этим шаблоном, дублируя визуал, который вы показываете на экране, но без какого-либо явного клонирования элементов пользовательского интерфейса.


Вышеупомянутое иллюстрирует, как точно использовать визуальное представление. Но, конечно, если у вас есть желание настроить вывод для печати, это так же просто, как объявление другого DataTemplate, которое будет использоваться при печати. ​​

Ответ 3

Я уверен, что должен быть простой способ сделать копию (кроме отсоединения от родителя, печати и прикрепления назад). Например, вы можете попробовать XamlWriter для записи xaml, а затем прочитать его с помощью XamlReader. Но я подозреваю, что могут быть некоторые ошибки привязки и компоновки таким образом.

Вместо этого я попытался бы использовать WriteableBitmap, чтобы сделать снимок области для печати и распечатать ее. Таким образом, вы создаете растровый и свободный вектор, но я недостаточно хорош в печати, чтобы сказать, хорошо это или плохо. В любом случае вы можете попробовать и проверить:).

Приветствия, Анвака.