diff --git a/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/dataobject.cs b/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/dataobject.cs
index 2b681df6f52..d2b0636c3f8 100644
--- a/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/dataobject.cs
+++ b/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/dataobject.cs
@@ -2222,12 +2222,14 @@ private static object EnsureBitmapDataFromFormat(string format, bool autoConvert
private const int DV_E_FORMATETC = unchecked((int)0x80040064);
private const int DV_E_LINDEX = unchecked((int)0x80040068);
- private const int DV_E_TYMED = unchecked((int)0x80040069);
+ internal const int DV_E_TYMED = unchecked((int)0x80040069);
private const int DV_E_DVASPECT = unchecked((int)0x8004006B);
private const int OLE_E_NOTRUNNING = unchecked((int)0x80040005);
private const int OLE_E_ADVISENOTSUPPORTED = unchecked((int)0x80040003);
private const int DATA_S_SAMEFORMATETC = unchecked((int)0x00040130);
private const int STG_E_MEDIUMFULL = unchecked((int)0x80030070);
+ internal const int CLIPBRD_E_CANT_OPEN = unchecked((int)0x800401D0);
+ internal const int CLIPBRD_E_BAD_DATA = unchecked((int)0x800401D3);
// Const integer base size of the file drop list: "4 + 8 + 4 + 4"
private const int FILEDROPBASESIZE = 20;
diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/PresentationCore.Tests.csproj b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/PresentationCore.Tests.csproj
index 3a4f208b437..afb7150dd77 100644
--- a/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/PresentationCore.Tests.csproj
+++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/PresentationCore.Tests.csproj
@@ -15,18 +15,84 @@
TargetFramework;TargetFrameworks
+
+
+
+
+
+
+
+ false
+ true
+ TargetFramework;TargetFrameworks
+
+
+ false
+ true
+ TargetFramework;TargetFrameworks
+
+
+ false
+ true
+ TargetFramework;TargetFrameworks
+
+
+ false
+ true
+ TargetFramework;TargetFrameworks
+
+
+
+
+
+
+
+ <_ClipboardTestNativeDependency
+ Include="$(ArtifactsDir)bin\PresentationNative\$(Platform)\$(Configuration)\PresentationNative_cor3.dll"
+ Condition="Exists('$(ArtifactsDir)bin\PresentationNative\$(Platform)\$(Configuration)\PresentationNative_cor3.dll')" />
+ <_ClipboardTestNativeDependency
+ Include="$(ArtifactsDir)bin\wpfgfx\$(Platform)\$(Configuration)\wpfgfx_cor3.dll"
+ Condition="Exists('$(ArtifactsDir)bin\wpfgfx\$(Platform)\$(Configuration)\wpfgfx_cor3.dll')" />
+ <_ClipboardTestNativeDependency
+ Include="$(ArtifactsDir)bin\D3DCompiler\$(Platform)\$(Configuration)\D3DCompiler_47_cor3.dll"
+ Condition="Exists('$(ArtifactsDir)bin\D3DCompiler\$(Platform)\$(Configuration)\D3DCompiler_47_cor3.dll')" />
+ <_ClipboardTestNativeDependency
+ Include="$(ArtifactsDir)bin\VCRuntime\$(Platform)\$(Configuration)\**\*.dll"
+ Condition="Exists('$(ArtifactsDir)bin\VCRuntime\$(Platform)\$(Configuration)')" />
+
+
+ <_ClipboardTestNativeDependency
+ Include="$(ArtifactsDir)bin\PresentationNative\Win32\$(Configuration)\PresentationNative_cor3.dll"
+ Condition="'$(Platform)' == 'x86' And Exists('$(ArtifactsDir)bin\PresentationNative\Win32\$(Configuration)\PresentationNative_cor3.dll')" />
+ <_ClipboardTestNativeDependency
+ Include="$(ArtifactsDir)bin\wpfgfx\Win32\$(Configuration)\wpfgfx_cor3.dll"
+ Condition="'$(Platform)' == 'x86' And Exists('$(ArtifactsDir)bin\wpfgfx\Win32\$(Configuration)\wpfgfx_cor3.dll')" />
+ <_ClipboardTestNativeDependency
+ Include="$(ArtifactsDir)bin\D3DCompiler\Win32\$(Configuration)\D3DCompiler_47_cor3.dll"
+ Condition="'$(Platform)' == 'x86' And Exists('$(ArtifactsDir)bin\D3DCompiler\Win32\$(Configuration)\D3DCompiler_47_cor3.dll')" />
+ <_ClipboardTestNativeDependency
+ Include="$(ArtifactsDir)bin\VCRuntime\Win32\$(Configuration)\**\*.dll"
+ Condition="'$(Platform)' == 'x86' And Exists('$(ArtifactsDir)bin\VCRuntime\Win32\$(Configuration)')" />
+
+
+
+
diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/ClipboardTests.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/ClipboardTests.cs
new file mode 100644
index 00000000000..09611b73489
--- /dev/null
+++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/ClipboardTests.cs
@@ -0,0 +1,1419 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Drawing.Imaging;
+using System.Runtime.InteropServices;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using Moq;
+
+namespace PresentationCore.Tests;
+
+[Collection(ClipboardCollection.Name)]
+public sealed class ClipboardTests
+{
+ private static readonly Lazy s_wpfGraphics = new(
+ () => NativeLibrary.Load(Path.Combine(AppContext.BaseDirectory, "wpfgfx_cor3.dll")));
+
+ // Ensures every string-format API rejects a null format before attempting clipboard access.
+ [Fact]
+ public void FormatTakingApis_NullFormat_ThrowArgumentNullException()
+ {
+ AssertArgumentNull("format", () => Clipboard.ContainsData(null!));
+ AssertArgumentNull("format", () => Clipboard.GetData(null!));
+ AssertArgumentNull("format", () => Clipboard.SetData(null!, "value"));
+ }
+
+ // Ensures every string-format API rejects the unsupported empty format consistently.
+ [Fact]
+ public void FormatTakingApis_EmptyFormat_ThrowArgumentException()
+ {
+ Assert.Throws(() => Clipboard.ContainsData(string.Empty));
+ Assert.Throws(() => Clipboard.GetData(string.Empty));
+ Assert.Throws(() => Clipboard.SetData(string.Empty, "value"));
+ }
+
+ // Verifies SetData validates its payload independently of format validation.
+ [Fact]
+ public void SetData_NullData_ThrowsArgumentNullException()
+ {
+ AssertArgumentNull("data", () => Clipboard.SetData("format", null!));
+ }
+
+ // Verifies each setter overload reports the documented parameter name for a null payload.
+ [Fact]
+ public void SetApis_NullData_ThrowArgumentNullException()
+ {
+ AssertArgumentNull("audioBytes", () => Clipboard.SetAudio((byte[])null!));
+ AssertArgumentNull("audioStream", () => Clipboard.SetAudio((Stream)null!));
+ AssertArgumentNull("fileDropList", () => Clipboard.SetFileDropList(null!));
+ AssertArgumentNull("image", () => Clipboard.SetImage(null!));
+ AssertArgumentNull("text", () => Clipboard.SetText(null!));
+ AssertArgumentNull("text", () => Clipboard.SetText(null!, TextDataFormat.Text));
+ AssertArgumentNull("data", () => Clipboard.SetDataObject(null!));
+ AssertArgumentNull("data", () => Clipboard.SetDataObject(null!, false));
+ }
+
+ // Confirms all text APIs reject values outside the defined TextDataFormat range.
+ [Theory]
+ [InlineData(-1)]
+ [InlineData(6)]
+ [InlineData(int.MaxValue)]
+ public void TextFormatApis_InvalidFormat_ThrowInvalidEnumArgumentException(int value)
+ {
+ TextDataFormat format = (TextDataFormat)value;
+
+ Assert.Equal("format", Assert.Throws(() => Clipboard.ContainsText(format)).ParamName);
+ Assert.Equal("format", Assert.Throws(() => Clipboard.GetText(format)).ParamName);
+ Assert.Equal("format", Assert.Throws(() => Clipboard.SetText("value", format)).ParamName);
+ }
+
+ // Verifies IsCurrent rejects a null IDataObject before performing an OLE query.
+ [Fact]
+ public void IsCurrent_NullData_ThrowsArgumentNullException()
+ {
+ AssertArgumentNull("data", () => Clipboard.IsCurrent(null!));
+ }
+
+ // Confirms IsCurrent short-circuits for managed IDataObject implementations that are not COM data objects.
+ [Fact]
+ public void IsCurrent_ManagedIDataObject_ReturnsFalseWithoutOleCall()
+ {
+ IDataObject dataObject = new Mock(MockBehavior.Strict).Object;
+
+ Assert.False(Clipboard.IsCurrent(dataObject));
+ }
+
+ // Verifies an empty file list is not accepted as clipboard file-drop data.
+ [Fact]
+ public void SetFileDropList_EmptyList_ThrowsArgumentException()
+ {
+ Assert.Throws(() => Clipboard.SetFileDropList(new StringCollection()));
+ }
+
+ // Verifies every file-drop entry must contain a non-null, non-empty path.
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ public void SetFileDropList_NullOrEmptyPath_ThrowsArgumentException(string? path)
+ {
+ StringCollection paths = new() { path! };
+
+ Assert.Throws(() => Clipboard.SetFileDropList(paths));
+ }
+
+ // Verifies malformed paths are rejected before file-drop data is placed on the clipboard.
+ [Fact]
+ public void SetFileDropList_InvalidPath_ThrowsArgumentException()
+ {
+ StringCollection paths = new() { "\0" };
+
+ Assert.Throws(() => Clipboard.SetFileDropList(paths));
+ }
+
+ // Documents that every OLE-backed Clipboard API requires an STA thread, including all typed overloads.
+ [Fact]
+ public void OleBackedApis_MtaThread_ThrowThreadStateException()
+ {
+ EnsureWpfGraphicsLoaded();
+ MemoryStream audioStream = new([1, 2, 3]);
+ DataObject dataObject = new("format", "value");
+ StringCollection fileDropList = new() { Path.Combine(Path.GetTempPath(), "clipboard.txt") };
+ WriteableBitmap image = new(1, 1, 96, 96, PixelFormats.Bgra32, palette: null);
+ Action[] actions =
+ [
+ Clipboard.Clear,
+ Clipboard.Flush,
+ () => Clipboard.GetAudioStream(),
+ () => Clipboard.GetDataObject(),
+ () => Clipboard.GetData("format"),
+ () => Clipboard.GetFileDropList(),
+ () => Clipboard.GetImage(),
+ () => Clipboard.GetText(),
+ () => Clipboard.GetText(TextDataFormat.Text),
+ () => Clipboard.SetAudio([1, 2, 3]),
+ () => Clipboard.SetAudio(audioStream),
+ () => Clipboard.SetData("format", "value"),
+ () => Clipboard.SetDataObject(dataObject),
+ () => Clipboard.SetDataObject(dataObject, true),
+ () => Clipboard.SetFileDropList(fileDropList),
+ () => Clipboard.SetImage(image),
+ () => Clipboard.SetText("value"),
+ () => Clipboard.SetText("value", TextDataFormat.Text),
+ () => Clipboard.IsCurrent(dataObject),
+ ];
+
+ foreach (Action action in actions)
+ {
+ Exception? exception = null;
+ Thread thread = new(() => exception = Xunit.Record.Exception(action));
+ thread.SetApartmentState(ApartmentState.MTA);
+ thread.Start();
+ thread.Join();
+
+ Assert.IsType(exception);
+ }
+ }
+
+ // Documents that Contains APIs use Win32 format probing and therefore remain valid on an MTA thread.
+ [Fact]
+ public void ContainsApis_MtaThread_DoNotRequireSta()
+ {
+ Action[] actions =
+ [
+ () => Clipboard.ContainsAudio(),
+ () => Clipboard.ContainsData("format"),
+ () => Clipboard.ContainsFileDropList(),
+ () => Clipboard.ContainsImage(),
+ () => Clipboard.ContainsText(),
+ () => Clipboard.ContainsText(TextDataFormat.Text),
+ ];
+
+ foreach (Action action in actions)
+ {
+ Exception? exception = null;
+ Thread thread = new(() => exception = Xunit.Record.Exception(action));
+ thread.SetApartmentState(ApartmentState.MTA);
+ thread.Start();
+ thread.Join();
+
+ Assert.Null(exception);
+ }
+ }
+
+ // Verifies Clear removes every typed view and leaves only an empty clipboard data object.
+ [StaFact]
+ public void Clear_PopulatedClipboard_ResetsAllTypedViews()
+ {
+ Clipboard.SetText("value");
+
+ Clipboard.Clear();
+
+ IDataObject dataObject = Assert.IsAssignableFrom(Clipboard.GetDataObject());
+ Assert.Empty(RetryClipboardAccess(dataObject.GetFormats));
+ Assert.False(Clipboard.ContainsAudio());
+ Assert.False(Clipboard.ContainsData(DataFormats.UnicodeText));
+ Assert.False(Clipboard.ContainsFileDropList());
+ Assert.False(Clipboard.ContainsImage());
+ Assert.False(Clipboard.ContainsText());
+ Assert.False(Clipboard.ContainsText(TextDataFormat.UnicodeText));
+ Assert.Null(Clipboard.GetAudioStream());
+ Assert.Null(Clipboard.GetData(DataFormats.UnicodeText));
+ Assert.Empty(Clipboard.GetFileDropList().Cast());
+ Assert.Null(Clipboard.GetImage());
+ Assert.Equal(string.Empty, Clipboard.GetText());
+ Assert.Equal(string.Empty, Clipboard.GetText(TextDataFormat.UnicodeText));
+ }
+
+ // Verifies the production Clipboard retry loop survives temporary native clipboard ownership by another thread.
+ [StaFact]
+ public void Clear_ClipboardTemporarilyLocked_RetriesUntilAvailable()
+ {
+ using ManualResetEventSlim clipboardOpened = new();
+ using ManualResetEventSlim releaseClipboard = new();
+ Exception? lockerException = null;
+
+ Thread locker = new(() =>
+ {
+ bool opened = false;
+ try
+ {
+ opened = OpenClipboardWithRetry();
+ clipboardOpened.Set();
+ releaseClipboard.Wait(TimeSpan.FromSeconds(5));
+ }
+ catch (Exception exception)
+ {
+ lockerException = exception;
+ clipboardOpened.Set();
+ }
+ finally
+ {
+ if (opened && !CloseClipboard())
+ {
+ lockerException ??= new Win32Exception(Marshal.GetLastWin32Error());
+ }
+ }
+ })
+ {
+ IsBackground = true,
+ Name = $"{nameof(ClipboardTests)} native clipboard owner"
+ };
+ locker.SetApartmentState(ApartmentState.MTA);
+ locker.Start();
+
+ Assert.True(clipboardOpened.Wait(TimeSpan.FromSeconds(5)));
+ Assert.Null(lockerException);
+
+ Thread releaser = new(() =>
+ {
+ Thread.Sleep(250);
+ releaseClipboard.Set();
+ })
+ {
+ IsBackground = true,
+ Name = $"{nameof(ClipboardTests)} native clipboard releaser"
+ };
+ releaser.Start();
+
+ try
+ {
+ // This call must initially receive CLIPBRD_E_CANT_OPEN and then succeed after the locker releases.
+ Clipboard.Clear();
+ }
+ finally
+ {
+ releaseClipboard.Set();
+ releaser.Join();
+ locker.Join();
+ }
+
+ Assert.Null(lockerException);
+ }
+
+ // Verifies custom formats register case-insensitively and round-trip through Clipboard and IDataObject APIs.
+ [StaFact]
+ public void SetData_CustomFormat_RoundTripsThroughIDataObject()
+ {
+ string format = $"WPF ClipboardTests {Guid.NewGuid():N}";
+ const string Value = "custom clipboard value";
+
+ try
+ {
+ Clipboard.SetData(format, Value);
+ DataFormat registeredFormat = DataFormats.GetDataFormat(format);
+
+ Assert.True(Clipboard.ContainsData(format));
+ Assert.True(Clipboard.ContainsData(format.ToUpperInvariant()));
+ Assert.Equal(Value, Clipboard.GetData(format));
+ Assert.Equal(Value, Clipboard.GetData(format.ToUpperInvariant()));
+ Assert.Equal(format, registeredFormat.Name);
+ Assert.Same(registeredFormat, DataFormats.GetDataFormat(registeredFormat.Id));
+ Assert.Same(registeredFormat, DataFormats.GetDataFormat(format.ToUpperInvariant()));
+
+ IDataObject dataObject = Assert.IsAssignableFrom(Clipboard.GetDataObject());
+ Assert.True(RetryClipboardAccess(() => dataObject.GetDataPresent(format)));
+ Assert.True(RetryClipboardAccess(() => dataObject.GetDataPresent(format, false)));
+ Assert.Equal(Value, RetryClipboardAccess(() => dataObject.GetData(format)));
+ Assert.Equal(Value, RetryClipboardAccess(() => dataObject.GetData(format, false)));
+ Assert.Contains(format, RetryClipboardAccess(dataObject.GetFormats));
+ Assert.Contains(format, RetryClipboardAccess(() => dataObject.GetFormats(false)));
+
+ Assert.False(Clipboard.ContainsAudio());
+ Assert.False(Clipboard.ContainsFileDropList());
+ Assert.False(Clipboard.ContainsImage());
+ Assert.False(Clipboard.ContainsText());
+ Assert.Null(Clipboard.GetAudioStream());
+ Assert.Empty(Clipboard.GetFileDropList().Cast());
+ Assert.Null(Clipboard.GetImage());
+ Assert.Equal(string.Empty, Clipboard.GetText());
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Confirms the default and copy:false overloads retain a live data source whose later mutations are observable.
+ [StaFact]
+ public void SetDataObject_DefaultAndFalseOverloads_PreserveLiveDataObject()
+ {
+ string format = $"WPF ClipboardTests {Guid.NewGuid():N}";
+ DataObject dataObject = new(format, "first");
+
+ try
+ {
+ Clipboard.SetDataObject(dataObject);
+
+ Assert.True(Clipboard.IsCurrent(dataObject));
+ dataObject.SetData(format, "first changed");
+ Assert.Equal("first changed", Clipboard.GetData(format));
+
+ Clipboard.Clear();
+ dataObject.SetData(format, "second");
+ Clipboard.SetDataObject(dataObject, copy: false);
+
+ Assert.True(Clipboard.IsCurrent(dataObject));
+ dataObject.SetData(format, "second changed");
+ Assert.Equal("second changed", Clipboard.GetData(format));
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Verifies replacing the clipboard owner updates IsCurrent for both the previous and new data objects.
+ [StaFact]
+ public void SetDataObject_ReplacingCurrentObject_UpdatesIsCurrent()
+ {
+ string format = $"WPF ClipboardTests {Guid.NewGuid():N}";
+ DataObject first = new(format, "first");
+ DataObject second = new(format, "second");
+
+ try
+ {
+ Clipboard.SetDataObject(first, copy: false);
+ Assert.True(Clipboard.IsCurrent(first));
+
+ Clipboard.SetDataObject(second, copy: false);
+
+ Assert.False(Clipboard.IsCurrent(first));
+ Assert.True(Clipboard.IsCurrent(second));
+ Assert.Equal("second", Clipboard.GetData(format));
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Verifies a live DataObject preserves the type-, format-, and auto-conversion IDataObject contracts.
+ [StaFact]
+ public void SetDataObject_LiveDataObject_PreservesIDataObjectOverloads()
+ {
+ string nativeFormat = $"WPF ClipboardTests {Guid.NewGuid():N}";
+ Uri uri = new("https://example.test/clipboard");
+ DataObject source = new();
+ source.SetData("typed string");
+ source.SetData(typeof(Uri), uri);
+ source.SetData(nativeFormat, "native", autoConvert: false);
+
+ try
+ {
+ Clipboard.SetDataObject(source, copy: false);
+
+ IDataObject result = Assert.IsAssignableFrom(Clipboard.GetDataObject());
+ Assert.True(result.GetDataPresent(typeof(string)));
+ Assert.True(result.GetDataPresent(typeof(Uri)));
+ Assert.True(result.GetDataPresent(nativeFormat));
+ Assert.True(result.GetDataPresent(nativeFormat, autoConvert: false));
+ Assert.Equal("typed string", result.GetData(typeof(string)));
+ Assert.Equal(uri, result.GetData(typeof(Uri)));
+ Assert.Equal("native", result.GetData(nativeFormat));
+ Assert.Equal("native", result.GetData(nativeFormat, autoConvert: false));
+ Assert.Contains(nativeFormat, result.GetFormats());
+ Assert.Contains(nativeFormat, result.GetFormats(autoConvert: false));
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Captures how the system clipboard synthesizes UnicodeText even when the source DataObject disables text auto-conversion.
+ [StaFact]
+ public void SetDataObject_AnsiText_ExposesSystemGeneratedUnicodeTextToClipboardApis()
+ {
+ const string Value = "ahoj";
+ DataObject source = new();
+ source.SetText(Value, TextDataFormat.Text);
+
+ try
+ {
+ Assert.False(source.GetDataPresent(DataFormats.UnicodeText));
+ Assert.False(source.GetDataPresent(DataFormats.UnicodeText, autoConvert: false));
+
+ Clipboard.SetDataObject(source);
+
+ IDataObject result = Assert.IsAssignableFrom(Clipboard.GetDataObject());
+ Assert.True(RetryClipboardAccess(() => result.GetDataPresent(DataFormats.UnicodeText)));
+ Assert.True(RetryClipboardAccess(() => result.GetDataPresent(DataFormats.UnicodeText, autoConvert: false)));
+ Assert.Equal(Value, RetryClipboardAccess(() => result.GetData(DataFormats.UnicodeText)));
+ Assert.Equal(Value, RetryClipboardAccess(() => result.GetData(DataFormats.UnicodeText, autoConvert: false)));
+
+ Assert.True(Clipboard.ContainsText());
+ Assert.True(Clipboard.ContainsText(TextDataFormat.Text));
+ Assert.True(Clipboard.ContainsText(TextDataFormat.UnicodeText));
+ Assert.Equal(Value, Clipboard.GetText());
+ Assert.Equal(Value, Clipboard.GetText(TextDataFormat.Text));
+ Assert.Equal(Value, Clipboard.GetText(TextDataFormat.UnicodeText));
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Verifies autoConvert controls text format advertisement while default GetData still searches the mapped format group.
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void DataObject_TextAutoConvert_ControlsMappedFormatAdvertisement(bool autoConvert)
+ {
+ const string Value = "text conversion verification";
+ string[] mappedFormats =
+ [
+ DataFormats.Text,
+ DataFormats.UnicodeText,
+ DataFormats.StringFormat,
+ ];
+ DataObject dataObject = new();
+ dataObject.SetData(DataFormats.Text, Value, autoConvert);
+
+ foreach (string format in mappedFormats)
+ {
+ bool expected = autoConvert || format == DataFormats.Text;
+
+ Assert.Equal(expected, dataObject.GetDataPresent(format));
+ Assert.Equal(format == DataFormats.Text, dataObject.GetDataPresent(format, autoConvert: false));
+ Assert.Equal(Value, dataObject.GetData(format));
+ Assert.Equal(format == DataFormats.Text ? Value : null, dataObject.GetData(format, autoConvert: false));
+ }
+
+ string[] expectedFormats = autoConvert ? mappedFormats : [DataFormats.Text];
+ Assert.Equal(expectedFormats.Order(), dataObject.GetFormats().Order());
+ Assert.Equal(new[] { DataFormats.Text }, dataObject.GetFormats(autoConvert: false));
+ }
+
+ // Verifies every built-in DataObject format mapping advertises all synonyms only when autoConvert is enabled.
+ [Fact]
+ public void DataObject_AutoConvert_ControlsAllBuiltInFormatMappings()
+ {
+ string[][] formatGroups =
+ [
+ [DataFormats.Text, DataFormats.UnicodeText, DataFormats.StringFormat],
+ [DataFormats.FileDrop, DataFormats.FileNameW, DataFormats.FileName],
+ [DataFormats.Bitmap, typeof(System.Drawing.Bitmap).FullName!, typeof(BitmapSource).FullName!],
+ [DataFormats.EnhancedMetafile, typeof(System.Drawing.Imaging.Metafile).FullName!],
+ ];
+
+ foreach (string[] formatGroup in formatGroups)
+ {
+ foreach (string sourceFormat in formatGroup)
+ {
+ foreach (bool autoConvert in new[] { false, true })
+ {
+ DataObject dataObject = new();
+ dataObject.SetData(sourceFormat, "value", autoConvert);
+ string[] expectedFormats = autoConvert ? formatGroup : [sourceFormat];
+
+ Assert.Equal(expectedFormats.Order(), dataObject.GetFormats().Order());
+ Assert.Equal(new[] { sourceFormat }, dataObject.GetFormats(autoConvert: false));
+
+ foreach (string targetFormat in formatGroup)
+ {
+ Assert.Equal(
+ autoConvert || targetFormat == sourceFormat,
+ dataObject.GetDataPresent(targetFormat));
+ Assert.Equal(
+ targetFormat == sourceFormat,
+ dataObject.GetDataPresent(targetFormat, autoConvert: false));
+ }
+ }
+ }
+ }
+ }
+
+ // Confirms SetDataObject wraps ordinary CLR values in a DataObject with their expected native formats.
+ [StaFact]
+ public void SetDataObject_RawObject_WrapsItInADataObject()
+ {
+ const string Value = "raw string";
+
+ try
+ {
+ Clipboard.SetDataObject(Value);
+
+ Assert.True(Clipboard.ContainsText());
+ Assert.Equal(Value, Clipboard.GetText());
+ IDataObject result = Assert.IsAssignableFrom(Clipboard.GetDataObject());
+ Assert.True(result.GetDataPresent(typeof(string)));
+ Assert.Equal(Value, result.GetData(typeof(string)));
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Verifies Flush materializes delayed data, releases ownership, and preserves the rendered value.
+ [StaFact]
+ public void Flush_NonPersistentDataObject_RendersDataAndReleasesSource()
+ {
+ string format = $"WPF ClipboardTests {Guid.NewGuid():N}";
+ DataObject dataObject = new(format, "persisted");
+
+ try
+ {
+ Clipboard.SetDataObject(dataObject, copy: false);
+ Assert.True(Clipboard.IsCurrent(dataObject));
+
+ Clipboard.Flush();
+
+ Assert.False(Clipboard.IsCurrent(dataObject));
+ Assert.Equal("persisted", Clipboard.GetData(format));
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Verifies copy:true snapshots the source immediately instead of retaining a live data object.
+ [StaFact]
+ public void SetDataObject_CopyTrue_RendersDataAndReleasesSource()
+ {
+ string format = $"WPF ClipboardTests {Guid.NewGuid():N}";
+ DataObject dataObject = new(format, "persisted");
+
+ try
+ {
+ Clipboard.SetDataObject(dataObject, copy: true);
+ dataObject.SetData(format, "changed after copy");
+
+ Assert.False(Clipboard.IsCurrent(dataObject));
+ Assert.Equal("persisted", Clipboard.GetData(format));
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Confirms the default text overload uses UnicodeText and preserves non-ASCII and surrogate-pair content.
+ [StaFact]
+ public void SetText_DefaultOverload_RoundTripsUnicodeText()
+ {
+ const string Value = "Clipboard \u03A9 \U0001F600";
+
+ try
+ {
+ Clipboard.SetText(Value);
+
+ Assert.True(Clipboard.ContainsText());
+ Assert.True(Clipboard.ContainsText(TextDataFormat.UnicodeText));
+ Assert.True(Clipboard.ContainsData(DataFormats.UnicodeText));
+ Assert.Equal(Value, Clipboard.GetText());
+ Assert.Equal(Value, Clipboard.GetText(TextDataFormat.UnicodeText));
+ Assert.Equal(Value, Clipboard.GetData(DataFormats.UnicodeText));
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Verifies each supported TextDataFormat maps to its corresponding clipboard format and round-trips unchanged.
+ [StaTheory]
+ [InlineData(TextDataFormat.Text)]
+ [InlineData(TextDataFormat.UnicodeText)]
+ [InlineData(TextDataFormat.Rtf)]
+ [InlineData(TextDataFormat.Html)]
+ [InlineData(TextDataFormat.CommaSeparatedValue)]
+ [InlineData(TextDataFormat.Xaml)]
+ public void SetText_AllSupportedFormats_RoundTrip(TextDataFormat format)
+ {
+ string value = $"text for {format}";
+ string dataFormat = GetDataFormat(format);
+
+ try
+ {
+ Clipboard.SetText(value, format);
+
+ Assert.True(Clipboard.ContainsText(format));
+ Assert.True(Clipboard.ContainsData(dataFormat));
+ Assert.Equal(value, Clipboard.GetText(format));
+ Assert.Equal(value, Clipboard.GetData(dataFormat));
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Confirms an empty string is valid clipboard text and remains distinguishable from an absent format.
+ [StaFact]
+ public void SetText_EmptyString_RoundTrips()
+ {
+ try
+ {
+ Clipboard.SetText(string.Empty);
+
+ Assert.True(Clipboard.ContainsText());
+ Assert.Equal(string.Empty, Clipboard.GetText());
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Verifies SetAudio(byte[]) snapshots the input buffer and exposes identical WaveAudio data.
+ [StaFact]
+ public void SetAudio_ByteArray_CopiesAndRoundTripsData()
+ {
+ byte[] source = [1, 2, 3, 4, 5];
+
+ try
+ {
+ Clipboard.SetAudio(source);
+ source[0] = 42;
+
+ Assert.True(Clipboard.ContainsAudio());
+ Assert.True(Clipboard.ContainsData(DataFormats.WaveAudio));
+ Assert.Equal(new byte[] { 1, 2, 3, 4, 5 }, ReadAllBytes(Clipboard.GetAudioStream()));
+ Assert.Equal(new byte[] { 1, 2, 3, 4, 5 }, ReadAllBytes(Assert.IsAssignableFrom(Clipboard.GetData(DataFormats.WaveAudio))));
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Verifies SetAudio(Stream) copies the complete stream, regardless of position, and outlives its source.
+ [StaFact]
+ public void SetAudio_Stream_CopiesWholeStreamAndSurvivesSourceDisposal()
+ {
+ MemoryStream source = new([6, 7, 8, 9]);
+ source.Position = 2;
+
+ try
+ {
+ Clipboard.SetAudio(source);
+ source.Dispose();
+
+ Assert.True(Clipboard.ContainsAudio());
+ Assert.Equal(new byte[] { 6, 7, 8, 9 }, ReadAllBytes(Clipboard.GetAudioStream()));
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Verifies file-drop paths and returned collections are copied rather than sharing mutable state.
+ [StaFact]
+ public void SetFileDropList_CopiesAndRoundTripsPaths()
+ {
+ string first = Path.Combine(Path.GetTempPath(), $"clipboard-{Guid.NewGuid():N}-1.txt");
+ string second = Path.Combine(Path.GetTempPath(), $"clipboard-{Guid.NewGuid():N}-2.txt");
+ StringCollection source = new() { first, second };
+
+ try
+ {
+ Clipboard.SetFileDropList(source);
+ source.Clear();
+
+ Assert.True(Clipboard.ContainsFileDropList());
+ Assert.True(Clipboard.ContainsData(DataFormats.FileDrop));
+ StringCollection returned = RetryClipboardAccess(Clipboard.GetFileDropList);
+ Assert.Equal(new[] { first, second }, returned.Cast());
+ Assert.Equal(
+ new[] { first, second },
+ Assert.IsType(RetryClipboardAccess(() => Clipboard.GetData(DataFormats.FileDrop))));
+
+ returned[0] = "changed";
+ returned.RemoveAt(1);
+
+ Assert.Equal(
+ new[] { first, second },
+ RetryClipboardAccess(Clipboard.GetFileDropList).Cast());
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Verifies SetImage snapshots source pixels and publishes a retrievable native bitmap representation.
+ [StaFact]
+ public void SetImage_CopiesAndRoundTripsPixels()
+ {
+ EnsureWpfGraphicsLoaded();
+ byte[] originalPixels =
+ [
+ 0x10, 0x20, 0x30, 0xFF,
+ 0x40, 0x50, 0x60, 0xFF,
+ ];
+ WriteableBitmap source = new(2, 1, 96, 96, PixelFormats.Bgra32, palette: null);
+ source.WritePixels(new Int32Rect(0, 0, 2, 1), originalPixels, stride: 8, offset: 0);
+
+ try
+ {
+ Clipboard.SetImage(source);
+ source.WritePixels(
+ new Int32Rect(0, 0, 2, 1),
+ new byte[]
+ {
+ 0x70, 0x80, 0x90, 0xFF,
+ 0xA0, 0xB0, 0xC0, 0xFF,
+ },
+ stride: 8,
+ offset: 0);
+
+ Assert.True(RetryClipboardAccess(Clipboard.ContainsImage));
+ Assert.True(RetryClipboardAccess(() => Clipboard.ContainsData(DataFormats.Bitmap)));
+ BitmapSource image = Assert.IsAssignableFrom(
+ RetryClipboardAccess(Clipboard.GetImage));
+ Assert.Equal(2, image.PixelWidth);
+ Assert.Equal(1, image.PixelHeight);
+ Assert.NotSame(source, image);
+ BitmapSource normalized = new FormatConvertedBitmap(image, PixelFormats.Bgr24, null, 0);
+ byte[] actualPixels = new byte[6];
+ normalized.CopyPixels(actualPixels, stride: 6, offset: 0);
+ Assert.Equal(
+ new byte[]
+ {
+ 0x10, 0x20, 0x30,
+ 0x40, 0x50, 0x60,
+ },
+ actualPixels);
+ Assert.IsAssignableFrom(
+ RetryClipboardAccess(() => Clipboard.GetData(DataFormats.Bitmap)));
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Captures that delayed EMF rendering returns a non-renderable copy and invalidates the source Metafile.
+ [StaFact]
+ public void SetDataObject_Metafile_CopyFalse_ReturnsNonRenderableCopyAndInvalidatesSource()
+ {
+ using Metafile source = EmfTestData.CreateMetafile();
+ DataObject sourceDataObject = new();
+ sourceDataObject.SetData(DataFormats.EnhancedMetafile, source, autoConvert: false);
+
+ try
+ {
+ Clipboard.SetDataObject(sourceDataObject, copy: false);
+
+ Assert.True(Clipboard.IsCurrent(sourceDataObject));
+ Assert.True(Clipboard.ContainsData(DataFormats.EnhancedMetafile));
+ IDataObject clipboardDataObject = Assert.IsAssignableFrom(
+ RetryClipboardAccess(Clipboard.GetDataObject));
+ Assert.True(RetryClipboardAccess(
+ () => clipboardDataObject.GetDataPresent(
+ DataFormats.EnhancedMetafile,
+ autoConvert: false)));
+ Assert.True(RetryClipboardAccess(
+ () => clipboardDataObject.GetDataPresent(
+ typeof(Metafile).FullName!,
+ autoConvert: true)));
+
+ using Metafile result = GetClipboardMetafile(DataFormats.EnhancedMetafile);
+ Assert.NotSame(source, result);
+ AssertMetafileIsNotRenderable(result);
+ Assert.Throws(() => source.GetMetafileHeader());
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Captures that Flush persists an EMF object that cannot be rendered after the source is disposed.
+ [StaFact]
+ public void SetDataObject_Metafile_CopyFalseFlush_ReturnsNonRenderableMetafile()
+ {
+ Metafile? source = EmfTestData.CreateMetafile();
+
+ try
+ {
+ DataObject sourceDataObject = new();
+ sourceDataObject.SetData(DataFormats.EnhancedMetafile, source, autoConvert: false);
+
+ try
+ {
+ Clipboard.SetDataObject(sourceDataObject, copy: false);
+ Assert.True(Clipboard.IsCurrent(sourceDataObject));
+ Clipboard.Flush();
+ Assert.False(Clipboard.IsCurrent(sourceDataObject));
+
+ source.Dispose();
+ source = null;
+
+ using Metafile result = GetClipboardMetafile(DataFormats.EnhancedMetafile);
+ AssertMetafileIsNotRenderable(result);
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+ finally
+ {
+ source?.Dispose();
+ }
+ }
+
+ // Captures that copy:true returns an EMF object that cannot be rendered after the source is disposed.
+ [StaFact]
+ public void SetDataObject_Metafile_CopyTrue_ReturnsNonRenderableMetafile()
+ {
+ Metafile? source = EmfTestData.CreateMetafile();
+
+ try
+ {
+ DataObject sourceDataObject = new();
+ sourceDataObject.SetData(DataFormats.EnhancedMetafile, source, autoConvert: false);
+
+ try
+ {
+ Clipboard.SetDataObject(sourceDataObject, copy: true);
+ Assert.False(Clipboard.IsCurrent(sourceDataObject));
+ source.Dispose();
+ source = null;
+
+ Assert.True(Clipboard.ContainsData(DataFormats.EnhancedMetafile));
+ using Metafile result = GetClipboardMetafile(DataFormats.EnhancedMetafile);
+ AssertMetafileIsNotRenderable(result);
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+ finally
+ {
+ source?.Dispose();
+ }
+ }
+
+ // Characterizes the distinct SetDataObject(object, copy) path that wraps a raw Metafile by its CLR type.
+ [StaTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void SetDataObject_RawMetafile_AdvertisesMappedFormatsAndInvalidatesSource(bool copy)
+ {
+ using Metafile source = EmfTestData.CreateMetafile();
+
+ try
+ {
+ Clipboard.SetDataObject(source, copy);
+ string mappedFormat = typeof(Metafile).FullName!;
+ IDataObject clipboardDataObject = Assert.IsAssignableFrom(
+ RetryClipboardAccess(Clipboard.GetDataObject));
+
+ Assert.Contains(
+ mappedFormat,
+ RetryClipboardAccess(() => clipboardDataObject.GetFormats(autoConvert: false)));
+ Assert.Contains(
+ DataFormats.EnhancedMetafile,
+ RetryClipboardAccess(() => clipboardDataObject.GetFormats(autoConvert: true)));
+ Assert.True(RetryClipboardAccess(
+ () => clipboardDataObject.GetDataPresent(mappedFormat, autoConvert: false)));
+ Assert.True(RetryClipboardAccess(
+ () => clipboardDataObject.GetDataPresent(
+ DataFormats.EnhancedMetafile,
+ autoConvert: true)));
+ Assert.True(RetryClipboardAccess(() => Clipboard.ContainsData(mappedFormat)));
+
+ COMException mappedException = Assert.Throws(
+ () => RetryClipboardAccess(() => Clipboard.GetData(mappedFormat)));
+ // A live object fails immediately with DV_E_TYMED. After copy:true, the failed eager render is surfaced
+ // by the system clipboard as CLIPBRD_E_BAD_DATA.
+ int expectedHResult = copy
+ ? DataObject.CLIPBRD_E_BAD_DATA
+ : DataObject.DV_E_TYMED;
+ Assert.Equal(expectedHResult, mappedException.HResult);
+
+ using Metafile result = GetClipboardMetafile(DataFormats.EnhancedMetafile);
+ AssertMetafileIsNotRenderable(result);
+ Assert.Throws(() => source.GetMetafileHeader());
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Captures that mapped-format auto-conversion returns a Metafile object whose native handle is not renderable.
+ [StaFact]
+ public void SetDataObject_Metafile_MappedFormatRequiresAutoConvert()
+ {
+ Metafile? source = EmfTestData.CreateMetafile();
+
+ try
+ {
+ DataObject sourceDataObject = new();
+ sourceDataObject.SetData(DataFormats.EnhancedMetafile, source, autoConvert: false);
+
+ try
+ {
+ Clipboard.SetDataObject(sourceDataObject, copy: true);
+ source.Dispose();
+ source = null;
+
+ IDataObject clipboardDataObject = Assert.IsAssignableFrom(
+ RetryClipboardAccess(Clipboard.GetDataObject));
+ string mappedFormat = typeof(Metafile).FullName!;
+
+ Assert.False(Clipboard.ContainsData(mappedFormat));
+ Assert.Null(Clipboard.GetData(mappedFormat));
+ Assert.True(RetryClipboardAccess(
+ () => clipboardDataObject.GetDataPresent(
+ DataFormats.EnhancedMetafile,
+ autoConvert: false)));
+ Assert.False(RetryClipboardAccess(
+ () => clipboardDataObject.GetDataPresent(
+ mappedFormat,
+ autoConvert: false)));
+ Assert.True(RetryClipboardAccess(
+ () => clipboardDataObject.GetDataPresent(
+ mappedFormat,
+ autoConvert: true)));
+ Assert.Contains(
+ DataFormats.EnhancedMetafile,
+ RetryClipboardAccess(() => clipboardDataObject.GetFormats(autoConvert: false)));
+ Assert.DoesNotContain(
+ mappedFormat,
+ RetryClipboardAccess(() => clipboardDataObject.GetFormats(autoConvert: false)));
+ Assert.Contains(
+ mappedFormat,
+ RetryClipboardAccess(() => clipboardDataObject.GetFormats(autoConvert: true)));
+ Assert.True(RetryClipboardAccess(
+ () => clipboardDataObject.GetDataPresent(typeof(Metafile))));
+
+ Assert.Null(RetryClipboardAccess(
+ () => clipboardDataObject.GetData(
+ mappedFormat,
+ autoConvert: false)));
+
+ using Metafile mappedResult = Assert.IsType(RetryClipboardAccess(
+ () => clipboardDataObject.GetData(
+ mappedFormat,
+ autoConvert: true)));
+ AssertMetafileIsNotRenderable(mappedResult);
+
+ using Metafile typedResult = Assert.IsType(RetryClipboardAccess(
+ () => clipboardDataObject.GetData(typeof(Metafile))));
+ AssertMetafileIsNotRenderable(typedResult);
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+ finally
+ {
+ source?.Dispose();
+ }
+ }
+
+ // Captures the Clipboard facade result for the mapped CLR format on a fresh live data object.
+ [StaFact]
+ public void SetDataObject_Metafile_AutoConvertTrue_ClipboardMappedGetThrowsDvETymed()
+ {
+ using Metafile source = EmfTestData.CreateMetafile();
+ DataObject sourceDataObject = new();
+ sourceDataObject.SetData(DataFormats.EnhancedMetafile, source, autoConvert: true);
+
+ try
+ {
+ Clipboard.SetDataObject(sourceDataObject, copy: false);
+ string mappedFormat = typeof(Metafile).FullName!;
+
+ Assert.True(Clipboard.ContainsData(mappedFormat));
+ COMException facadeException = Assert.Throws(
+ () => RetryClipboardAccess(() => Clipboard.GetData(mappedFormat)));
+ Assert.Equal(DataObject.DV_E_TYMED, facadeException.HResult);
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Captures the IDataObject result for the mapped CLR format without a preceding facade retrieval.
+ [StaFact]
+ public void SetDataObject_Metafile_AutoConvertTrue_IDataObjectMappedGetThrowsDvETymed()
+ {
+ using Metafile source = EmfTestData.CreateMetafile();
+ DataObject sourceDataObject = new();
+ sourceDataObject.SetData(DataFormats.EnhancedMetafile, source, autoConvert: true);
+
+ try
+ {
+ Clipboard.SetDataObject(sourceDataObject, copy: false);
+ string mappedFormat = typeof(Metafile).FullName!;
+ IDataObject clipboardDataObject = Assert.IsAssignableFrom(
+ RetryClipboardAccess(Clipboard.GetDataObject));
+
+ Assert.True(RetryClipboardAccess(
+ () => clipboardDataObject.GetDataPresent(
+ DataFormats.EnhancedMetafile,
+ autoConvert: false)));
+ Assert.True(RetryClipboardAccess(
+ () => clipboardDataObject.GetDataPresent(
+ mappedFormat,
+ autoConvert: false)));
+ COMException exception = Assert.Throws(
+ () => RetryClipboardAccess(
+ () => clipboardDataObject.GetData(
+ mappedFormat,
+ autoConvert: false)));
+ Assert.Equal(DataObject.DV_E_TYMED, exception.HResult);
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Captures distinct managed lifetimes: disposing one non-renderable result does not alter the other result's behavior.
+ [StaFact]
+ public void SetDataObject_Metafile_RepeatedGetsReturnIndependentlyDisposableResults()
+ {
+ using Metafile source = EmfTestData.CreateMetafile();
+ DataObject sourceDataObject = new();
+ sourceDataObject.SetData(DataFormats.EnhancedMetafile, source, autoConvert: false);
+
+ try
+ {
+ Clipboard.SetDataObject(sourceDataObject, copy: true);
+ Metafile? first = GetClipboardMetafile(DataFormats.EnhancedMetafile);
+ Metafile? second = null;
+ try
+ {
+ second = GetClipboardMetafile(DataFormats.EnhancedMetafile);
+ Assert.NotSame(first, second);
+ AssertMetafileIsNotRenderable(first);
+ first.Dispose();
+ first = null;
+ AssertMetafileIsNotRenderable(second);
+ }
+ finally
+ {
+ first?.Dispose();
+ second?.Dispose();
+ }
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Captures that a valid non-public EMF stream is not consumed but produces CLIPBRD_E_BAD_DATA on retrieval.
+ [StaFact]
+ public void SetDataObject_ValidNonExposableMetafileStream_CopyTrue_GetDataThrowsClipboardBadData()
+ {
+ byte[] bytes;
+ using (Metafile metafile = EmfTestData.CreateMetafile())
+ {
+ bytes = EmfTestData.GetBytes(metafile);
+ }
+
+ using MemoryStream sourceStream = EmfTestData.CreateNonExposableStream(bytes);
+ sourceStream.Position = Math.Min(7, sourceStream.Length);
+ long originalPosition = sourceStream.Position;
+ DataObject sourceDataObject = new();
+ sourceDataObject.SetData(DataFormats.EnhancedMetafile, sourceStream, autoConvert: false);
+
+ try
+ {
+ Clipboard.SetDataObject(sourceDataObject, copy: true);
+
+ Assert.Equal(originalPosition, sourceStream.Position);
+ AssertClipboardBadData(() => Clipboard.GetData(DataFormats.EnhancedMetafile));
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Captures that a valid exposable EMF stream returns a non-renderable Metafile without moving the stream.
+ [StaFact]
+ public void SetDataObject_ValidExposableMetafileStream_CopyTrue_ReturnsNonRenderableMetafile()
+ {
+ byte[] bytes;
+ using (Metafile metafile = EmfTestData.CreateMetafile())
+ {
+ bytes = EmfTestData.GetBytes(metafile);
+ }
+
+ using MemoryStream source = EmfTestData.CreateExposableStream(bytes);
+ source.Position = Math.Min(7, source.Length);
+ long originalPosition = source.Position;
+ DataObject sourceDataObject = new();
+ sourceDataObject.SetData(DataFormats.EnhancedMetafile, source, autoConvert: false);
+
+ try
+ {
+ Clipboard.SetDataObject(sourceDataObject, copy: true);
+
+ Assert.Equal(originalPosition, source.Position);
+ using Metafile result = GetClipboardMetafile(DataFormats.EnhancedMetafile);
+ AssertMetafileIsNotRenderable(result);
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Captures that GetBuffer's trailing capacity bytes are accepted but still produce a non-renderable Metafile.
+ [StaFact]
+ public void SetDataObject_OverallocatedExposableMetafileStream_CopyTrue_ReturnsNonRenderableMetafile()
+ {
+ byte[] bytes;
+ using (Metafile metafile = EmfTestData.CreateMetafile())
+ {
+ bytes = EmfTestData.GetBytes(metafile);
+ }
+
+ using MemoryStream source = EmfTestData.CreateOverallocatedExposableStream(bytes);
+ DataObject sourceDataObject = new();
+ sourceDataObject.SetData(DataFormats.EnhancedMetafile, source, autoConvert: false);
+
+ try
+ {
+ Clipboard.SetDataObject(sourceDataObject, copy: true);
+
+ using Metafile result = GetClipboardMetafile(DataFormats.EnhancedMetafile);
+ AssertMetafileIsNotRenderable(result);
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Captures that only MemoryStream receives the .NET 9 native EMF stream treatment.
+ [StaFact]
+ public void SetDataObject_NonMemoryMetafileStream_CopyTrue_GetDataThrowsClipboardBadData()
+ {
+ byte[] bytes;
+ using (Metafile metafile = EmfTestData.CreateMetafile())
+ {
+ bytes = EmfTestData.GetBytes(metafile);
+ }
+
+ using MemoryStream inner = new(bytes, writable: false);
+ using BufferedStream source = new(inner);
+ DataObject sourceDataObject = new();
+ sourceDataObject.SetData(DataFormats.EnhancedMetafile, source, autoConvert: false);
+
+ try
+ {
+ Clipboard.SetDataObject(sourceDataObject, copy: true);
+
+ AssertClipboardBadData(() => Clipboard.GetData(DataFormats.EnhancedMetafile));
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Captures the in-process delayed-rendering exception for a non-exposable MemoryStream.
+ [StaFact]
+ public void SetDataObject_ValidNonExposableMetafileStream_CopyFalse_GetDataThrowsUnauthorizedAccessException()
+ {
+ byte[] bytes;
+ using (Metafile metafile = EmfTestData.CreateMetafile())
+ {
+ bytes = EmfTestData.GetBytes(metafile);
+ }
+
+ using MemoryStream source = EmfTestData.CreateNonExposableStream(bytes);
+ DataObject sourceDataObject = new();
+ sourceDataObject.SetData(DataFormats.EnhancedMetafile, source, autoConvert: false);
+
+ try
+ {
+ Clipboard.SetDataObject(sourceDataObject, copy: false);
+
+ Assert.Throws(
+ () => RetryClipboardAccess(
+ () => Clipboard.GetData(DataFormats.EnhancedMetafile)));
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Captures that publishing WMF as EnhancedMetafile produces CLIPBRD_E_BAD_DATA and invalidates the WMF source.
+ [StaFact]
+ public void SetDataObject_WmfAsEnhancedMetafile_CopyTrue_DoesNotReturnEmf()
+ {
+ using MemoryStream stream = new(EmfTestData.CreateWmfBytes(), writable: false);
+ using Metafile source = new(stream);
+ Assert.True(source.GetMetafileHeader().IsWmf());
+ DataObject sourceDataObject = new();
+ sourceDataObject.SetData(DataFormats.EnhancedMetafile, source, autoConvert: false);
+
+ try
+ {
+ Clipboard.SetDataObject(sourceDataObject, copy: true);
+
+ AssertClipboardBadData(() => Clipboard.GetData(DataFormats.EnhancedMetafile));
+ Assert.Throws(() => source.GetMetafileHeader());
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Captures the behavior of the Clipboard.SetData convenience API for a real EMF payload.
+ [StaFact]
+ public void SetData_Metafile_ReturnsNonRenderableMetafile()
+ {
+ using Metafile source = EmfTestData.CreateMetafile();
+
+ try
+ {
+ Clipboard.SetData(DataFormats.EnhancedMetafile, source);
+
+ Assert.True(Clipboard.ContainsData(DataFormats.EnhancedMetafile));
+ using Metafile result = GetClipboardMetafile(DataFormats.EnhancedMetafile);
+ AssertMetafileIsNotRenderable(result);
+ Assert.Throws(() => source.GetMetafileHeader());
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Verifies Clear removes EMF availability, retrieval state, and ownership even when assertions fail.
+ [StaFact]
+ public void Clear_EnhancedMetafile_RemovesData()
+ {
+ using Metafile source = EmfTestData.CreateMetafile();
+ DataObject sourceDataObject = new();
+ sourceDataObject.SetData(DataFormats.EnhancedMetafile, source, autoConvert: false);
+
+ try
+ {
+ Clipboard.SetDataObject(sourceDataObject, copy: false);
+ Assert.True(Clipboard.ContainsData(DataFormats.EnhancedMetafile));
+ Assert.True(Clipboard.IsCurrent(sourceDataObject));
+
+ Clipboard.Clear();
+
+ Assert.False(Clipboard.IsCurrent(sourceDataObject));
+ Assert.False(Clipboard.ContainsData(DataFormats.EnhancedMetafile));
+ Assert.Null(Clipboard.GetData(DataFormats.EnhancedMetafile));
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ // Captures empty and malformed publicly exposable EMF stream behavior.
+ [StaTheory]
+ [InlineData(new byte[] { })]
+ [InlineData(new byte[] { 1, 2, 3 })]
+ public void SetDataObject_InvalidMetafileStream_CopyTrue_GetDataThrowsClipboardBadData(byte[] bytes)
+ {
+ using MemoryStream source = EmfTestData.CreateExposableStream(bytes);
+ DataObject sourceDataObject = new();
+ sourceDataObject.SetData(DataFormats.EnhancedMetafile, source, autoConvert: false);
+
+ try
+ {
+ Clipboard.SetDataObject(sourceDataObject, copy: true);
+
+ AssertClipboardBadData(() => Clipboard.GetData(DataFormats.EnhancedMetafile));
+ }
+ finally
+ {
+ Clipboard.Clear();
+ }
+ }
+
+ private static byte[] ReadAllBytes(Stream stream)
+ {
+ Assert.NotNull(stream);
+ if (stream.CanSeek)
+ {
+ stream.Position = 0;
+ }
+
+ using MemoryStream copy = new();
+ stream.CopyTo(copy);
+ return copy.ToArray();
+ }
+
+ private static void EnsureWpfGraphicsLoaded() =>
+ _ = s_wpfGraphics.Value;
+
+ private static Metafile GetClipboardMetafile(string format) =>
+ Assert.IsType(RetryClipboardAccess(() => Clipboard.GetData(format)));
+
+ private static void AssertMetafileIsNotRenderable(Metafile metafile) =>
+ Assert.Throws(() => EmfTestData.AssertValid(metafile));
+
+ private static void AssertClipboardBadData(Action action)
+ {
+ COMException exception = Assert.Throws(
+ () => RetryClipboardAccess(
+ () =>
+ {
+ action();
+ return true;
+ }));
+ Assert.Equal(DataObject.CLIPBRD_E_BAD_DATA, exception.HResult);
+ }
+
+ private static T RetryClipboardAccess(Func operation)
+ {
+ for (int attemptsRemaining = 10; ; attemptsRemaining--)
+ {
+ try
+ {
+ return operation();
+ }
+ catch (COMException exception) when (
+ exception.HResult == DataObject.CLIPBRD_E_CANT_OPEN
+ && attemptsRemaining > 1)
+ {
+ Thread.Sleep(100);
+ }
+ }
+ }
+
+ private static bool OpenClipboardWithRetry()
+ {
+ for (int attemptsRemaining = 10; attemptsRemaining > 0; attemptsRemaining--)
+ {
+ if (OpenClipboard(0))
+ {
+ return true;
+ }
+
+ Thread.Sleep(100);
+ }
+
+ throw new Win32Exception(Marshal.GetLastWin32Error());
+ }
+
+ private static void AssertArgumentNull(string paramName, Action action) =>
+ Assert.Equal(paramName, Assert.Throws(action).ParamName);
+
+ private static string GetDataFormat(TextDataFormat format) =>
+ format switch
+ {
+ TextDataFormat.Text => DataFormats.Text,
+ TextDataFormat.UnicodeText => DataFormats.UnicodeText,
+ TextDataFormat.Rtf => DataFormats.Rtf,
+ TextDataFormat.Html => DataFormats.Html,
+ TextDataFormat.CommaSeparatedValue => DataFormats.CommaSeparatedValue,
+ TextDataFormat.Xaml => DataFormats.Xaml,
+ _ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(TextDataFormat)),
+ };
+
+ [DllImport("user32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool OpenClipboard(nint newOwner);
+
+ [DllImport("user32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool CloseClipboard();
+}
+
+[CollectionDefinition(Name, DisableParallelization = true)]
+public sealed class ClipboardCollection
+{
+ public const string Name = "System clipboard serial";
+}
diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/DataObjectMetafileTests.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/DataObjectMetafileTests.cs
new file mode 100644
index 00000000000..1edf8a0648c
--- /dev/null
+++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/DataObjectMetafileTests.cs
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Drawing.Imaging;
+
+namespace PresentationCore.Tests;
+
+public sealed class DataObjectMetafileTests
+{
+ // Defines the managed DataObject auto-conversion contract independently from the system clipboard.
+ [Theory]
+ [InlineData("EnhancedMetafile", "System.Drawing.Imaging.Metafile")]
+ [InlineData("System.Drawing.Imaging.Metafile", "EnhancedMetafile")]
+ public void GetData_MetafileMappedFormat_RespectsAutoConvert(
+ string sourceFormat,
+ string mappedFormat)
+ {
+ using Metafile source = EmfTestData.CreateMetafile();
+ DataObject dataObject = new();
+ dataObject.SetData(sourceFormat, source, autoConvert: true);
+
+ Assert.True(dataObject.GetDataPresent(sourceFormat, autoConvert: false));
+ Assert.False(dataObject.GetDataPresent(mappedFormat, autoConvert: false));
+ Assert.True(dataObject.GetDataPresent(mappedFormat, autoConvert: true));
+ Assert.Same(source, dataObject.GetData(sourceFormat, autoConvert: false));
+ Assert.Null(dataObject.GetData(mappedFormat, autoConvert: false));
+ Assert.Same(source, dataObject.GetData(mappedFormat, autoConvert: true));
+ Assert.Contains(sourceFormat, dataObject.GetFormats(autoConvert: false));
+ Assert.Contains(mappedFormat, dataObject.GetFormats(autoConvert: true));
+ EmfTestData.AssertValid(source);
+ }
+}
diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/EmfTestData.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/EmfTestData.cs
new file mode 100644
index 00000000000..72317259e21
--- /dev/null
+++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/EmfTestData.cs
@@ -0,0 +1,245 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Buffers.Binary;
+using System.ComponentModel;
+using System.Drawing.Imaging;
+using System.Runtime.InteropServices;
+
+namespace PresentationCore.Tests;
+
+// Creates deterministic EMF/WMF payloads and validates native records without relying on external test assets.
+internal static class EmfTestData
+{
+ internal static Rectangle Frame { get; } = new(0, 0, 120, 80);
+
+ // Records a rectangle into an in-memory EMF, then transfers ownership of an independent HENHMETAFILE.
+ public static Metafile CreateMetafile()
+ {
+ using MemoryStream stream = new();
+ using Bitmap referenceBitmap = new(1, 1);
+ using Graphics referenceGraphics = Graphics.FromImage(referenceBitmap);
+
+ Metafile recording;
+ nint hdc = referenceGraphics.GetHdc();
+ try
+ {
+ recording = new(
+ stream,
+ hdc,
+ Frame,
+ MetafileFrameUnit.Pixel,
+ EmfType.EmfOnly);
+ }
+ finally
+ {
+ referenceGraphics.ReleaseHdc(hdc);
+ }
+
+ using (recording)
+ {
+ using Graphics graphics = Graphics.FromImage(recording);
+ nint recordingHdc = graphics.GetHdc();
+ try
+ {
+ Assert.NotEqual(0, GdiRectangle(recordingHdc, 10, 10, 110, 62));
+ }
+ finally
+ {
+ graphics.ReleaseHdc(recordingHdc);
+ }
+ }
+
+ using MemoryStream source = new(stream.ToArray(), writable: false);
+ using Metafile reader = new(source);
+ nint handle = reader.GetHenhmetafile();
+ if (handle == 0)
+ {
+ throw new Win32Exception();
+ }
+
+ try
+ {
+ return new Metafile(handle, deleteEmf: true);
+ }
+ catch
+ {
+ DeleteEnhMetaFile(handle);
+ throw;
+ }
+ }
+
+ // Copies the native EMF bits. GetHenhmetafile transfers a handle that this method must release.
+ public static byte[] GetBytes(Metafile metafile)
+ {
+ nint handle = metafile.GetHenhmetafile();
+ if (handle == 0)
+ {
+ throw new Win32Exception();
+ }
+
+ bool completed = false;
+ try
+ {
+ uint size = GetEnhMetaFileBits(handle, 0, null);
+ if (size == 0)
+ {
+ throw new Win32Exception();
+ }
+
+ byte[] bytes = new byte[size];
+ if (GetEnhMetaFileBits(handle, size, bytes) != size)
+ {
+ throw new Win32Exception();
+ }
+
+ completed = true;
+ return bytes;
+ }
+ finally
+ {
+ bool deleted = DeleteEnhMetaFile(handle);
+ if (completed)
+ {
+ Assert.True(deleted);
+ }
+ }
+ }
+
+ // The production .NET 9 path calls MemoryStream.GetBuffer, so public buffer visibility changes behavior.
+ public static MemoryStream CreateNonExposableStream(byte[] bytes) =>
+ new(bytes, 0, bytes.Length, writable: false, publiclyVisible: false);
+
+ public static MemoryStream CreateExposableStream(byte[] bytes) =>
+ new(bytes, 0, bytes.Length, writable: false, publiclyVisible: true);
+
+ // GetBuffer returns the full capacity, not Length. This shape exposes trailing capacity bytes to production.
+ public static MemoryStream CreateOverallocatedExposableStream(byte[] bytes)
+ {
+ MemoryStream stream = new(bytes.Length + 32);
+ stream.Write(bytes);
+ stream.Position = 0;
+ return stream;
+ }
+
+ // Converts the deterministic EMF to WMF bits and adds the placeable header required by GDI+ stream loading.
+ public static byte[] CreateWmfBytes()
+ {
+ using Metafile emf = CreateMetafile();
+ nint emfHandle = emf.GetHenhmetafile();
+ if (emfHandle == 0)
+ {
+ throw new Win32Exception();
+ }
+
+ using Bitmap referenceBitmap = new(1, 1);
+ using Graphics referenceGraphics = Graphics.FromImage(referenceBitmap);
+ nint referenceHdc = 0;
+ bool completed = false;
+ try
+ {
+ referenceHdc = referenceGraphics.GetHdc();
+ const int MmAnisotropic = 8;
+ uint size = GetWinMetaFileBits(emfHandle, 0, null, MmAnisotropic, referenceHdc);
+ if (size == 0)
+ {
+ throw new Win32Exception();
+ }
+
+ byte[] wmfBits = new byte[size];
+ if (GetWinMetaFileBits(emfHandle, size, wmfBits, MmAnisotropic, referenceHdc) != size)
+ {
+ throw new Win32Exception();
+ }
+
+ byte[] result = AddPlaceableWmfHeader(wmfBits);
+ completed = true;
+ return result;
+ }
+ finally
+ {
+ try
+ {
+ if (referenceHdc != 0)
+ {
+ referenceGraphics.ReleaseHdc(referenceHdc);
+ }
+ }
+ finally
+ {
+ bool deleted = DeleteEnhMetaFile(emfHandle);
+ if (completed)
+ {
+ Assert.True(deleted);
+ }
+ }
+ }
+ }
+
+ // Verifies structure and meaningful drawing records rather than accepting any non-null Metafile.
+ public static void AssertValid(Metafile metafile)
+ {
+ MetafileHeader header = metafile.GetMetafileHeader();
+ Assert.True(header.IsEmfOrEmfPlus());
+ Assert.True(header.Bounds.Width > 0);
+ Assert.True(header.Bounds.Height > 0);
+
+ List records = [];
+ using Bitmap target = new(Frame.Width, Frame.Height);
+ using Graphics graphics = Graphics.FromImage(target);
+ graphics.EnumerateMetafile(
+ metafile,
+ System.Drawing.Point.Empty,
+ (recordType, _, _, _, _) =>
+ {
+ records.Add(recordType);
+ return true;
+ });
+
+ Assert.Contains(EmfPlusRecordType.EmfHeader, records);
+ Assert.Contains(EmfPlusRecordType.EmfRectangle, records);
+ Assert.Contains(EmfPlusRecordType.EmfEof, records);
+ }
+
+ // Prepends an Aldus placeable header and computes its XOR checksum over the first ten WORDs.
+ private static byte[] AddPlaceableWmfHeader(byte[] wmfBits)
+ {
+ const int HeaderSize = 22;
+ byte[] result = new byte[HeaderSize + wmfBits.Length];
+ Span header = result.AsSpan(0, HeaderSize);
+ BinaryPrimitives.WriteUInt32LittleEndian(header, 0x9AC6CDD7);
+ BinaryPrimitives.WriteInt16LittleEndian(header[6..], (short)Frame.Left);
+ BinaryPrimitives.WriteInt16LittleEndian(header[8..], (short)Frame.Top);
+ BinaryPrimitives.WriteInt16LittleEndian(header[10..], (short)Frame.Right);
+ BinaryPrimitives.WriteInt16LittleEndian(header[12..], (short)Frame.Bottom);
+ BinaryPrimitives.WriteUInt16LittleEndian(header[14..], 1440);
+
+ ushort checksum = 0;
+ for (int offset = 0; offset < 20; offset += sizeof(ushort))
+ {
+ checksum ^= BinaryPrimitives.ReadUInt16LittleEndian(header[offset..]);
+ }
+
+ BinaryPrimitives.WriteUInt16LittleEndian(header[20..], checksum);
+ wmfBits.CopyTo(result, HeaderSize);
+ return result;
+ }
+
+ [DllImport("gdi32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool DeleteEnhMetaFile(nint hEnhMetaFile);
+
+ [DllImport("gdi32.dll", SetLastError = true)]
+ private static extern uint GetEnhMetaFileBits(nint hEnhMetaFile, uint bufferSize, [Out] byte[]? buffer);
+
+ [DllImport("gdi32.dll", SetLastError = true)]
+ private static extern uint GetWinMetaFileBits(
+ nint hEnhMetaFile,
+ uint bufferSize,
+ [Out] byte[]? buffer,
+ int mapMode,
+ nint referenceHdc);
+
+ [DllImport("gdi32.dll", EntryPoint = "Rectangle", SetLastError = true)]
+ private static extern int GdiRectangle(nint hdc, int left, int top, int right, int bottom);
+}