Skip to content

InkCanvas.CopySelection returns empty ContentControl XAML when copying non‑stroke child elements #11881

Description

@jheraldP

Description

A possible regression appears to have been introduced after the changes in PR dotnet/wpf#11797.
When copying a selected child element in an InkCanvas—specifically a non‑stroke item such as a ContentControl—the clipboard XAML no longer contains the element’s content.

Previously, InkCanvas.CopySelection() produced clipboard data containing:

  • The InkCanvas
  • The selected ContentControl
  • The ContentControl.Content (expected)

After recent Windows updates containing this fix, the clipboard now contains:

  • The InkCanvas
  • A ContentControl with empty content

This breaks scenarios where applications rely on copying embedded UI elements inside an InkCanvas.

In NET 10 Runtime 10.0.5, the CopySelection succeeds and the clipboard XAML contains the ContentControl with its Content preserved.

Reproduction Steps

  1. Create an InkCanvas.
  2. Set the InkCanvas EditingMode set to Select)
  3. Add a child ContentControl with DataTemplate (e.g., a TextBlock, Image, or custom control).
  4. Select the child element.
  5. Call InkCanvas.CopySelection().
  6. Inspect clipboard data via Clipboard.GetText(TextDataFormat.Xaml).

MainWindow.xaml

<Window x:Class="InkCanvasTest.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:InkCanvasTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="480" Width="720">
    <Window.Resources>
        <Style x:Key="NoteTextItemTextBoxWithCustomCaretStyle" TargetType="{x:Type local:NoteTextItemTextBoxWithCustomCaret}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type local:NoteTextItemTextBoxWithCustomCaret}">
                        <Grid Background="RosyBrown" >
                            <TextBox x:Name="PART_TextBox"
                                     Text="{Binding Text, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                                     Background="Transparent"
                                     BorderThickness="0"
                                     Padding="2"/>
                            <Canvas x:Name="PART_Canvas" IsHitTestVisible="False">
                                <Border x:Name="PART_BorderCaret" Background="Black" Width="1" Height="16" Visibility="Collapsed" />
                            </Canvas>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <DataTemplate DataType="{x:Type local:TextItem}">
            <local:NoteTextItemTextBoxWithCustomCaret Style="{StaticResource NoteTextItemTextBoxWithCustomCaretStyle}"
                                                      Width="200" Height="80"
                                                      InkCanvas.Left="50" InkCanvas.Top="50"
                                                      Text="{Binding TextVal}" />
        </DataTemplate>
    </Window.Resources>
    <Grid Margin="8">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="150"/>
        </Grid.RowDefinitions>

        <StackPanel Orientation="Horizontal" Grid.Row="0" Margin="0,0,0,8">
            <Button Name="CopyButton" Click="CopyButton_Click" Margin="0,0,8,0">CopySelection</Button>
            <Button Name="ShowClipboardButton" Click="ShowClipboardButton_Click" Margin="0,0,8,0">Show Clipboard XAML</Button>
            <Button Name="AddChildButton" Click="AddChildButton_Click" Margin="0,0,8,0">Add ContentControl Child</Button>
            <TextBlock VerticalAlignment="Center" Foreground="Gray"> — Select a child by clicking it (InkCanvas in Select mode)</TextBlock>
        </StackPanel>

        <InkCanvas Name="inkCanvas"
               Grid.Row="1"
               Background="#FFF2F2F2"
               EditingMode="Select"
               Margin="0,0,0,8">
        </InkCanvas>

        <TextBox Name="clipboardTextBox"
             Grid.Row="2"
             AcceptsReturn="True"
             VerticalScrollBarVisibility="Auto"
             TextWrapping="Wrap"
             FontFamily="Consolas"
             IsReadOnly="True"/>
    </Grid>
</Window>

MainWindow.xaml.cs

using System.Windows;
using System.Windows.Controls;

namespace InkCanvasTest
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        //public TextItem _TextItem;
        public MainWindow()
        {
            InitializeComponent();
            var _TextItem = new TextItem();
            _TextItem.TextVal = "Hello, custom caret!";
            var contentControl = new ContentControl { Content = _TextItem };
            InkCanvas.SetLeft(contentControl, 50);
            InkCanvas.SetTop(contentControl, 50);
            inkCanvas.Children.Add(contentControl);
        }

        private void CopyButton_Click(object sender, RoutedEventArgs e)
        {
            // Call CopySelection — user should have selected a child (click it) before pressing this.
            try
            {
                inkCanvas.CopySelection();
                MessageBox.Show("CopySelection called. Use 'Show Clipboard XAML' to inspect clipboard XAML.");
                string content = Clipboard.GetText(TextDataFormat.Xaml);
                MessageBox.Show("CopySelection threw: " + content);

            }
            catch (Exception ex)
            {
                MessageBox.Show("CopySelection threw: " + ex);
            }
        }

        private void ShowClipboardButton_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                var data = Clipboard.GetDataObject();
                if (data == null)
                {
                    clipboardTextBox.Text = "<Clipboard is empty>";
                    return;
                }

                // Prefer DataFormats.Xaml if present, otherwise show available formats
                if (data.GetDataPresent(DataFormats.Xaml))
                {
                    //var xaml = data.GetData(DataFormats.Xaml) as string;
                    var xaml = data.GetData(DataFormats.Xaml) as string;
                    clipboardTextBox.Text = xaml ?? "<null>";
                }
                else if (data.GetDataPresent(DataFormats.XamlPackage))
                {
                    // XamlPackage is a binary package; show the available formats and note it's present
                    clipboardTextBox.Text = $"Clipboard contains {DataFormats.XamlPackage} (non-string data). Available formats:\r\n{string.Join(", ", data.GetFormats())}";
                }
                else
                {
                    clipboardTextBox.Text = "Clipboard does not contain XAML format. Available formats:\r\n" + string.Join(", ", data.GetFormats());
                }
            }
            catch (Exception ex)
            {
                clipboardTextBox.Text = ex.ToString();
            }
        }

        private void AddChildButton_Click(object sender, RoutedEventArgs e)
        {
            var _TextItem = new TextItem();
            _TextItem.TextVal = "Custom Caret Added child " + DateTime.Now.ToLongTimeString();
            var cc = new ContentControl { Width = 160, Height = 80 };
            cc.Content = _TextItem;
            cc.BorderBrush = System.Windows.Media.Brushes.Black;
            cc.BorderThickness = new Thickness(1);
            InkCanvas.SetLeft(cc, 240);
            InkCanvas.SetTop(cc, 50);
            inkCanvas.Children.Add(cc);
        }
    }


    public class NoteTextItemTextBoxWithCustomCaret : Control
    {
        private Canvas? canvas;
        private Border? borderCaret;
        private TextBox? innerTextBox;
        public NoteTextItemTextBoxWithCustomCaret()
        {
            this.Focusable = true;
        }

        public static readonly DependencyProperty TextProperty =
            DependencyProperty.Register(
                nameof(Text),
                typeof(string),
                typeof(NoteTextItemTextBoxWithCustomCaret),
                new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

        public string Text
        {
            get => (string)GetValue(TextProperty);
            set => SetValue(TextProperty, value);
        }

        static NoteTextItemTextBoxWithCustomCaret()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(NoteTextItemTextBoxWithCustomCaret),
                new FrameworkPropertyMetadata(typeof(NoteTextItemTextBoxWithCustomCaret)));
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            canvas = GetTemplateChild("PART_Canvas") as Canvas;
            innerTextBox = GetTemplateChild("PART_TextBox") as TextBox;
            borderCaret = GetTemplateChild("PART_BorderCaret") as Border;

            if (innerTextBox != null)
            {
                // Ensure the TextBox text is bound to the control's Text property via template binding,
                // but guard here that we still have the reference to attach handlers.
                innerTextBox.SelectionChanged += InnerTextBox_SelectionChanged;
                innerTextBox.SizeChanged += InnerTextBox_SizeChanged;
                innerTextBox.LostFocus += (s, o) =>
                {
                    if (borderCaret != null) borderCaret.Visibility = Visibility.Collapsed;
                };
                innerTextBox.GotFocus += (s, o) =>
                {
                    if (borderCaret != null) borderCaret.Visibility = Visibility.Visible;
                };
            }

            if (borderCaret != null)
            {
                borderCaret.Width = 1;
                borderCaret.Visibility = Visibility.Collapsed;
            }
        }

        private void InnerTextBox_SizeChanged(object? sender, SizeChangedEventArgs e)
        {
            if (innerTextBox == null || canvas == null || borderCaret == null)
                return;

            canvas.Width = innerTextBox.Width;
            canvas.Height = innerTextBox.Height;

            UpdateCaretPosition();
        }

        private void InnerTextBox_SelectionChanged(object? sender, RoutedEventArgs e)
        {
            UpdateCaretPosition();
        }

        private void UpdateCaretPosition()
        {
            if (innerTextBox == null || canvas == null || borderCaret == null)
                return;

            try
            {
                Rect caretRect = innerTextBox.GetRectFromCharacterIndex(innerTextBox.CaretIndex);

                if (!double.IsInfinity(caretRect.X) && caretRect.Right < innerTextBox.ActualWidth)
                {
                    Canvas.SetLeft(borderCaret, caretRect.X);
                }
                else if (caretRect.Right > innerTextBox.ActualWidth)
                {
                    Canvas.SetLeft(borderCaret, Math.Max(0, innerTextBox.ActualWidth - borderCaret.Width));
                }

                if (!double.IsInfinity(caretRect.Y))
                {
                    Canvas.SetTop(borderCaret, caretRect.Y);
                }
            }
            catch
            {
                // GetRectFromCharacterIndex can throw if layout not ready — swallow here.
            }
        }
    }

    public class TextItem
    {
        public string TextVal { get; set; }
    }
}

Expected behavior

Clipboard XAML should include the selected ContentControl with its content preserved

Actual behavior

Clipboard XAML contains an empty ContentControl with no child content.

Regression?

It work previously on .NET Framework 4.6.2 before August Updates. and Dot NET 10 Runtime 10.0.5

Known Workarounds

  • Call InkCanvas.CopySelection() normally.
  • Retrieve the clipboard XAML and deserialize it into an InkCanvas.
  • Serialize the selected child element using XamlWriter.Save.
  • Deserialize the serialized child and add it to the InkCanvas’s Children collection.
  • Serialize the corrected InkCanvas and write it back to the clipboard.

Impact

This can affect copy/paste workflows that expect visual children (e.g., ContentControls) to preserve their content. Impact: user-visible regression for any scenario that copies non-stroke children from InkCanvas (clipboard-based copy/paste, inter-app copy/paste).

Configuration

Which version of .NET is the code running on?

  • .NET Framework 4.6.2 with August 2026 Updates and Dot NET 10.0.303

What OS and version, and what distro if applicable?

  • Windows 11 25H2 (OS Build 26200.9168)

What is the architecture (x64, x86, ARM, ARM64)?

  • x86

Other information

No response

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugProduct bug (most likely)Cost:MWork that requires one engineer up to 2 weeksPriority:1Work that is critical for the release, but we could probably ship withoutregressionstatus: This issue is a regression from a previous build or release

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions