Search code examples
c#wpfmvvmdatagridclipboard

How do I implement a clipboard copy on a WPF DataGrid using MVVM?


I have a DataGrid backed by an ObservableCollection of objects in my ViewModel. I also have a context menu with a Copy entry which uses the default Copy command. I'd like to be able to copy data from the DataGrid, but when I click on the Copy menu item, WPF throws this exception:

OpenClipboard Failed (Exception from HRESULT: 0x800301D0 (CLIPBRD_E_CANT_OPEN))

ViewModel

public class ViewModel
{
  public class Person
  {
    public string FirstName { get; set; }
    public string LastName { get; set; }
  }

  public ObservableCollection<Person> People { get; set; }

  public ViewModel()
  {
    People = new ObservableCollection<Person>
    {
      new Person {FirstName = "Heir", LastName = "Band"},
      new Person {FirstName = "Rose", LastName = "Anne"},
      new Person {FirstName = "Tim", LastName = "Poral"}
    };
  }
}

XAML

<Window x:Class="WpfApp1.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:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:ViewModel />
    </Window.DataContext>

    <Grid>
        <DataGrid ItemsSource="{Binding Path=People}">
            <DataGrid.ContextMenu>
                <ContextMenu>
                    <MenuItem Header="Copy" Command="Copy" />
                </ContextMenu>
            </DataGrid.ContextMenu>
        </DataGrid>
    </Grid>
</Window>

I've seen other descriptions of this exception. However:

  • I'm using Visual Studio 2017, not 2019.
  • I'm not using SetText, or SetDataObject, or calling anything for that matter.
  • The exception happens every time, not intermittently, no matter if I'm running from within Visual Studio or not.
  • I'm not interested in code-behind such as "myDataGrid_CopyingRowClipboardContent" since I'm using MVVM.

More Information - 10/17/2019

I did some more digging after Abin Mathew posted his answer. While his answer is an excellent one, and it works--it doesn't use WPF Commanding, but rather uses RelayCommand. That's fine. This question doesn't specify that WPF Commanding must be used.

However, I still wanted to know why WPF Commanding Copy wasn't working for a DataGrid. In fact, it does work--it just depends on timing. If you run the exact code I posted above, but put breakpoints at System.Windows.Clipbard.Flush and System.Windows.Controls.DataGrid.OnExecutedCopy, and then click the run button each time the breakpoints are hit, the copy will succeed:

Successful copy using breakpoints

So in conclusion:

  1. RelayCommand works for performing a copy.
  2. WPF Commanding also "works".
  3. WPF Commanding manages getting the data destined for the clipboard from the selected DataGrid rows itself.
  4. There's some sort of race condition going on that is causing WPF Commanding to fail to get access to Flush the clipboard when copying from a DataGrid.
  5. Either I need more code to prevent this race condition, or Microsoft has introduced a bug breaking WPF Commanding Copy from a DataGrid.

Solution

  • ApplicationCommands like Cut & Copy only act on Selection.if you are not able to select text like in TextBox then it will throw exception.

    I am afraid that WPF introduced a new Bug

    for example like below

        <TextBox>
            <TextBox.ContextMenu>
                <ContextMenu>
                    <MenuItem Header="Paste" Command="ApplicationCommands.Paste" />                    
                    <MenuItem Header="Copy" Command="ApplicationCommands.Copy" />
                </ContextMenu>
            </TextBox.ContextMenu>
        </TextBox>
    

    You can use copy on DataGrid without selection by adding ICommand and Binding the Command Like below

    <MenuItem Header="Copy" Command="{Binding CopyCommand}" CommandParameter="{Binding}"
    

    And the ViewModel will be

            public ICommand CopyCommand => new RelayCommand<object>(Copy);
    
            private static void Copy(object obj)
            {
                Clipboard.SetDataObject(((ViewModel)obj).People);
            }
    

    This will copy the collection of People to the Clipboard. If that is what you are trying to do.

    Hope this helps.