Drag and Drop in UWP in list of bank accounts

2019-06-23 16:11发布

问题:

I have a Universal Windows Application for a local bank, I'm working on a money transfer view, and they need to transfer money from account to account using the Drag and Drop feature in UWP applications.

I've made the animation part, but I need help after I drop the list item to the "Account To" list.

I'll attach a screenshot to make it clear.

As you see in the picture, I need to drag one item from the "From Account" list and drop it on only one item on "To Account" list. How can I achieve this ?

回答1:

I've created a small sample which shows drag-drop between two ListViews filled with some Accounts. I will skip the implementation of UserControls - the Page xaml looks like this:

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid.RowDefinitions>
        <RowDefinition Height="200"/>
        <RowDefinition Height="200"/>
    </Grid.RowDefinitions>

    <ListView Header="Source" Margin="10" Grid.Row="0" CanDragItems="True" ItemsSource="{x:Bind Accounts}" SelectionMode="None">
        <ListView.ItemsPanel>
            <ItemsPanelTemplate>
                <ItemsStackPanel Orientation="Horizontal"/>
            </ItemsPanelTemplate>
        </ListView.ItemsPanel>
        <ListView.ItemTemplate>
            <DataTemplate>
                <controls:AccountControl CanDrag="True" DragStarting="AccountControl_DragStarting"/>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>

    <ListView Header="Targets" Margin="10" Grid.Row="1" ItemsSource="{x:Bind Accounts}" SelectionMode="None">
        <ListView.ItemsPanel>
            <ItemsPanelTemplate>
                <ItemsStackPanel Orientation="Horizontal"/>
            </ItemsPanelTemplate>
        </ListView.ItemsPanel>
        <ListView.ItemTemplate>
            <DataTemplate>
                <controls:AccountControl AllowDrop="True" DragEnter="AccountControl_DragEnter" Drop="AccountControl_Drop"/>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</Grid>

As you can see there is a Source list in which the control is firing an event when it's being dragged.

private void AccountControl_DragStarting(UIElement sender, DragStartingEventArgs args)
{
    if ((sender as AccountControl)?.DataContext is Account account)
    {
        args.AllowedOperations = DataPackageOperation.Link;
        args.Data.SetData(accountId, account.Id);
    }
}

The Account class apart from name and balance has a Guid identifier so I can use it to pass information which source account has been used in transfer method.

The items in second list (Targets) accepts only drop operation and for this purpose fire two events:

private void AccountControl_DragEnter(object sender, DragEventArgs e)
{
    e.AcceptedOperation = DataPackageOperation.Link;
    e.DragUIOverride.Caption = "Transfer";
}

private async void AccountControl_Drop(object sender, DragEventArgs e)
{
    if ((e.OriginalSource as AccountControl)?.DataContext is Account targetAccount)
        if (await (e.DataView.GetDataAsync(accountId)) is Guid sourceAccountId)
        {
            var sourceAccount = Accounts.First(x => x.Id == sourceAccountId);
            sourceAccount.Balance -= 1000;
            targetAccount.Balance += 1000;
        }
}

The first one sets accepted operation and some information for the user. The second one 'transfers' some money from one account to the second.

Everything looks like this:

Some more help you can find at MS directly, other article and in MS samples repository.



回答2:

I am not fully satisfied with the "solutions" which I will provide. They are much likely very far away from the ideal implementations, but ...

The XAML code which I created to try to replicate as easily, but also consistently your object, consisted in a group of draggable Rectangles inside a StackPanel Control, plus another StackPanel Control where the items could be dragged into.

     <Grid>
        <Grid.Resources>
            <Style TargetType="Rectangle">
                <Setter Property="Width" Value="300"/>
                <Setter Property="Height" Value="300"/>
                <Setter Property="CanDrag" Value="True"/>
            </Style>
        </Grid.Resources>

        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <StackPanel Name="StackPanelRectangles"  Grid.Row="0" Orientation="Horizontal">
            <Rectangle x:Name="RedRect" Fill="Red" DragStarting="Rectangle_DragStarting"/>
            <Rectangle x:Name="GreenRect" Fill="Green" DragStarting="Rectangle_DragStarting"/>
            <Rectangle x:Name="BlueRect" Fill="Blue" DragStarting="Rectangle_DragStarting"/>
        </StackPanel>

        <StackPanel Name="StackPanelDropArea" Background="Azure" AllowDrop="True" 
                    DragOver="StackPanel_DragOver" Drop="StackPanel_Drop" 
                    Grid.Row="2" Orientation="Horizontal"
                    HorizontalAlignment="Center">
            <TextBlock>Drop anywhere in this area area</TextBlock>
        </StackPanel> 
    </Grid>

1st Solution:

I routed every DragStarting event of the multiple Rectangles to the same EventHandler. In this EventHandler, we have access to the UIElement which is being dragged, so with an exposed property of type UIElement in your Page class, and you can simply clone the necessary properties for when you need to drop it, like this:

UIElement dragItem;
private void Rectangle_DragStarting(UIElement sender, DragStartingEventArgs args)
{
   dataPackage.RequestedOperation = DataPackageOperation.Copy;
   dragItem = sender;
}

Then when the item is dropped EventHandler is called, I have simply add it onto my DropArea.

 private void StackPanel_Drop(object sender, DragEventArgs e)
 {
    Rectangle newElement = new Rectangle();
    newElement.Width =  (dragItem as Rectangle).Width;
    newElement.Height = (dragItem as Rectangle).Height;
    newElement.Fill = (dragItem as Rectangle).Fill;
    StackPanelDropArea.Children.Add(newElement);
 }

You cannot add your new Control by setting to reference the object being dragged, since there are properties such as the respective Parent which will thrown an exception when you try to add the Control to a different container.

2nd Solution: I was extremely focused on on utilizing the DataPackage object, and one of its supported default formats, but I don't think any of them can actually hold data of an Object, such as our UIElement.

But each DataPackage instance supports a set of properties, which corresponds a Dictionary. We can set the Dictionary to hold UIElement in there, as long as we specify a key to reference that same object later on.

private void Rectangle_DragStarting(UIElement sender, DragStartingEventArgs args)
{
   dataPackage.RequestedOperation = DataPackageOperation.Copy;
   args.Data.Properties.Add("myRectangle", sender);
}

In the drop Event Handler, you can obtain the UIElement, like such:

private async void StackPanel_Drop(object sender, DragEventArgs e)
{
   Rectangle element = e.DataView.Properties["myRectangle"] as Rectangle;
                       ......
                       ......
}

3rd Solution:

This solution used the method SetText(String) exposed by DataPackage, to hold the value of the Name property of the UIElement being dragged.

private void Rectangle_DragStarting(UIElement sender, DragStartingEventArgs args)
{
    dataPackage = new DataPackage();
    dataPackage.RequestedOperation = DataPackageOperation.Copy;
    Rectangle rectangle = sender as Rectangle;
    dataPackage.SetText(rectangle.Name);
    Clipboard.SetContent(dataPackage);
}

By knowing the value of the Name property of the UIElement which is being dragged, looked for it, by using the VisualTreeHelper Class, like this:

private async void StackPanel_Drop(object sender, DragEventArgs e)
{
    DataPackageView dataPackageView = Clipboard.GetContent();
    if (dataPackageView.Contains(StandardDataFormats.Text))
    {
        draggedObject = await dataPackageView.GetTextAsync();
    }

    // Dragged objects come from another one of our Parent's Children
    DependencyObject parent = VisualTreeHelper.GetParent(StackPanelDropArea);
    int count = VisualTreeHelper.GetChildrenCount(parent);

    for(int i=0; i< count; i++)
    {
        DependencyObject child = VisualTreeHelper.GetChild(parent, i);
        if(child.GetType().Equals(typeof(StackPanel)))
        {
            StackPanel currentStackPanel = child as StackPanel;
            if(currentStackPanel.Name == "StackPanelRectangles")
            {
                int numberOfRectangles = VisualTreeHelper.GetChildrenCount(currentStackPanel);
                for(int j=0; j<numberOfRectangles; j++)
                {
                    if(VisualTreeHelper.GetChild(currentStackPanel,j).GetType().Equals(typeof(Rectangle)))
                    {
                        Rectangle currentRectangle = VisualTreeHelper.GetChild(currentStackPanel, j) as Rectangle;
                        if (draggedObject != string.Empty && currentRectangle.Name.Equals(draggedObject))
                        {
                            Rectangle newRectangle = new Rectangle();
                            newRectangle.Width = currentRectangle.Width;
                            newRectangle.Height = currentRectangle.Height;
                            newRectangle.Fill = currentRectangle.Fill;

                            StackPanelDropArea.Children.Add(newRectangle);
                        }
                    }

                }
            }
        }
    } */
}

Result:



回答3:

I usually try tackling this several times before giving up and using a third party library. The one I typically use is:

https://github.com/punker76/gong-wpf-dragdrop



回答4:

You may subscribe to PointerPressed event in your DataTemplate and extract all the things you need.

XAML:

    <DataTemplate x:Name="DataTemplate">
        <Grid Background="Transparent" PointerPressed="Grid_OnPointerPressed"/>
    </DataTemplate>

Code:

    private void Grid_OnPointerPressed(object sender, PointerRoutedEventArgs e)
    {
        //your FrameworkElement
        var frameworkElement = sender as FrameworkElement;
        //global position of your element
        var itemPosition = frameworkElement.TransformToVisual(Window.Current.Content).TransformPoint(new Point(0, 0)).ToVector2();
        //your data
        var selectedItemData = frameworkElement.DataContext as ItemData;
    }

Save your data, use UWP Drag'n'Drop. On drop load your data.