Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question: How to block a drop using the DragOver event in TreeView or TreeViewItem? #1154

Closed
dpaulino opened this issue Aug 11, 2019 · 45 comments · Fixed by #1646
Closed

Question: How to block a drop using the DragOver event in TreeView or TreeViewItem? #1154

dpaulino opened this issue Aug 11, 2019 · 45 comments · Fixed by #1646
Assignees
Labels

Comments

@dpaulino
Copy link
Contributor

dpaulino commented Aug 11, 2019

I have a TreeView that contains two types of tree items, Leaf and Group.

Leaves can be children of groups.
Groups can be children of groups.
Leaves cannot have children.

Within the same tree, I want to allow my customers to reorder the tree and to move items to different groups by dragging and dropping elements. Based on the rules provided above, I need to be able to conditionally block certain drop operations, and I want to block them during the DragOver event. I have tried the following code, but it does nothing to block the drop:

            <c:TreeView ItemsSource="{x:Bind ViewModel.Nodes}"
                        DragOver="TreeView_DragOver"
                        DragEnter="TreeView_DragEnter"
                        DragItemsCompleted="TreeView_DragItemsCompleted"
                        DragItemsStarting="TreeView_DragItemsStarting">
                <c:TreeView.ItemTemplate>
                    <DataTemplate x:DataType="local:ExplorerItem">
                        <local:MyTreeViewItem ItemsSource="{x:Bind Children}" Content="{x:Bind Name}" AllowDrop="True" DragOver="TreeViewItem_DragOver"
                                        DragEnter="TreeViewItem_DragEnter"
                                        Drop="TreeViewItem_Drop" DropCompleted="TreeViewItem_DropCompleted"/>
                    </DataTemplate>
                </c:TreeView.ItemTemplate>
            </c:TreeView>
        private void TreeViewItem_DragOver(object sender, DragEventArgs e)
        {
            // this does not do anything
            e.AcceptedOperation = DataPackageOperation.None;
        }

        private void TreeView_DragOver(object sender, DragEventArgs e)
        {
            // this does not do anything
            e.AcceptedOperation = DataPackageOperation.None;
        }

        private void TreeViewItem_DragEnter(object sender, DragEventArgs e)
        {
            // this does not do anything
            e.AcceptedOperation = DataPackageOperation.None;
        }

        private void TreeView_DragEnter(object sender, DragEventArgs e)
        {
            // this does not do anything
            e.AcceptedOperation = DataPackageOperation.None;
        }

Am I doing something wrong? Can someone show me how to conditionally block a drop operation?

@kaiguo
Copy link
Contributor

kaiguo commented Aug 13, 2019

@dpaulino I tried a few things looks like the easiest way to do this is updating CanReorderItems in DragItemsStarting.

private void TreeView_DragItemsStarting(Microsoft.UI.Xaml.Controls.TreeView sender, Microsoft.UI.Xaml.Controls.TreeViewDragItemsStartingEventArgs args)
{
    YourTreeView.CanReorderItems = /*isItemDraggable*/;
}

@dpaulino
Copy link
Contributor Author

If I were to use the drag items starting event, do I have knowledge of what the drop target is going to be? I would need to know the drop target in order to decide if the drop should be blocked, right?

To clarify my question, here are some scenarios.

Given a tree that looks like this

  • group 1
    • leaf a
    • leaf b
  • leaf c
  • group 2
  1. User can move leaf c into group 2
  2. User can move group 1 into group 2
  3. User can move leaf b into the root of the tree
  4. User cannot move group 2 into leaf b.

You can see that in scenario 4, I would need to block the drop. This is where I would like to use the drag over event. When the user picks up group 2 and hovers it over leaf b, I want the tree view to show the 🚫 icon to express that this drop operation is not allowed.

Does the drag items started event work in this scenario?

@kaiguo
Copy link
Contributor

kaiguo commented Aug 13, 2019

If I were to use the drag items starting event, do I have knowledge of what the drop target is going to be? I would need to know the drop target in order to decide if the drop should be blocked, right?

To clarify my question, here are some scenarios.

Given a tree that looks like this

  • group 1

    • leaf a
    • leaf b
  • leaf c

  • group 2

  1. User can move leaf c into group 2
  2. User can move group 1 into group 2
  3. User can move leaf b into the root of the tree
  4. User cannot move group 2 into leaf b.

You can see that in scenario 4, I would need to block the drop. This is where I would like to use the drag over event. When the user picks up group 2 and hovers it over leaf b, I want the tree view to show the 🚫 icon to express that this drop operation is not allowed.

Does the drag items started event work in this scenario?

I see, I think you can move the logic to DragOver in this case

private void TreeViewItem_DragOver(object sender, DragEventArgs e)
{
        TreeViewItem treeViewItem = sender as TreeViewItem;
        var item = YourTreeView.ItemFromContainer(treeViewItem) as YourItemType;
        TestTreeView.CanReorderItems = item.IsGroup;
}

The RequestedOperation and AcceptedOperation didn't really work because TreeView is changing those values internally when CanReorderItems is true, you can take a look at the code and see what's its doing, but setting the value to false should block the reorder/drop completely.

@kaiguo
Copy link
Contributor

kaiguo commented Aug 15, 2019 via email

@dpaulino
Copy link
Contributor Author

dpaulino commented Aug 15, 2019

I deleted my previous comment because I found out I was doing something wrong.

But now I'm back with a new comment. I have tried this approach:

private void TreeViewItem_DragOver(object sender, DragEventArgs e)
{
        TreeViewItem treeViewItem = sender as TreeViewItem;
        var item = YourTreeView.ItemFromContainer(treeViewItem) as YourItemType;
        TestTreeView.CanReorderItems = item.IsGroup;
}

It sort of works, but it seems I am no longer able to reorder the list. Example below.

Say I have a tree that looks like this:

  • Leaf 1
  • Leaf 2
  • Leaf 3
  • Leaf 4

Using the approach you provided, I am unable to move Leaf 1 to between Leaf 2 and 3. The reason seems to be that in the DragOver event, the Tree.CanReorderItems is set to false as the event handler is completing, so I am unable to reorder the leaves.

Any ideas on how to allow reordering but still keep all the previous requirements? Also, is there a fully functional drag drop sample with conditional dropping somewhere? That would make things a lot easier if someone could just make a fully functional sample for a TreeView that can perform conditional dropping.

@kaiguo
Copy link
Contributor

kaiguo commented Aug 15, 2019

Try setting CanReorderItems back to true in DragLeave?

private void TreeViewItem_DragLeave(object sender, DragEventArgs e)
{
    TestTreeView.CanReorderItems = true;
}

@kaiguo
Copy link
Contributor

kaiguo commented Aug 15, 2019

Hmm, looks like that doesn't work. You might have to override those drag&drop functions and handle everything on your own. Let me double check tomorrow and see if there are better alternatives...

@dpaulino
Copy link
Contributor Author

Yeah I have tried that and it does not work.

Would you or someone else be able to build a small sample that uses the TreeView with ItemsSource bound to an ObservableCollection and with conditional drop? I've been trying to make this work for 3 months and I have just had so many problems. I'm desperate to have this ability because my customers have been complaining so much about this.

@kaiguo
Copy link
Contributor

kaiguo commented Aug 15, 2019

I made a sample app here: https://github.com/kaiguo/TreeViewConditionalReorderSample

Updating AcceptedOperation in event callbacks doesn't work because the handler is triggered after the actual event and TreeViewItem has already done a bunch of stuff when you get to the callback. But overriding the events in TreeViewItem does work since you get a chance to make changes before TreeViewItem executing its own code.

@dpaulino
Copy link
Contributor Author

This sample looks promising. I just tried it and I got a crash while performing a drag. However, I can no longer reproduce it... I'm going to experiment with this a bit more and I will report back if I can isolate the crash.

@dpaulino
Copy link
Contributor Author

Hmm. When I perform a drop onto a group, the observable collection is not being updated. Can you confirm?

@kaiguo
Copy link
Contributor

kaiguo commented Aug 21, 2019

Hmm. When I perform a drop onto a group, the observable collection is not being updated. Can you confirm?

Looks like it's working for me. I added a button in the sample app to dump out ItemsSource, you can take a look.

@dpaulino
Copy link
Contributor Author

Ah I see what's going on. Your sample is using the prerelease package. When I reverted your sample to the regular release package, the observable collection was not being updated.

image

So that bug was fixed in the prerelease package. Let me try updating my app to the prerelease package and I'll try this again.

@dpaulino
Copy link
Contributor Author

Weird, I'm getting a C++ exception on startup after I updated to that prerelease package. Here's the tail end of the debug output

'Nightingale.exe' (Win32): Loaded 'C:\Windows\System32\CryptoWinRT.dll'. 
'Nightingale.exe' (Win32): Loaded 'C:\Users\kid_j\Source\Repos\Nightingale\Package\bin\x64\Debug\AppX\Microsoft.UI.Xaml.Markup.winmd'. 
'Nightingale.exe' (CoreCLR: CoreCLR_UWP_Domain): Loaded 'C:\Users\kid_j\Source\Repos\nightingale\Package\bin\x64\Debug\AppX\Microsoft.UI.Xaml.Markup.winmd'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
The thread 0x2bbc has exited with code 0 (0x0).
'Nightingale.exe' (Win32): Loaded 'C:\Users\kid_j\Source\Repos\Nightingale\Package\bin\x64\Debug\AppX\Microsoft.UI.Xaml.winmd'. Module was built without symbols.
'Nightingale.exe' (Win32): Unloaded 'C:\Users\kid_j\Source\Repos\Nightingale\Package\bin\x64\Debug\AppX\Microsoft.UI.Xaml.winmd'
'Nightingale.exe' (Win32): Loaded 'C:\Users\kid_j\Source\Repos\Nightingale\Package\bin\x64\Debug\AppX\Microsoft.UI.Xaml.winmd'. Module was built without symbols.
'Nightingale.exe' (CoreCLR: CoreCLR_UWP_Domain): Loaded 'C:\Users\kid_j\Source\Repos\nightingale\Package\bin\x64\Debug\AppX\Microsoft.UI.Xaml.winmd'. Module was built without symbols.
'Nightingale.exe' (Win32): Loaded 'C:\Users\kid_j\Source\Repos\Nightingale\Package\bin\x64\Debug\AppX\Microsoft.UI.Xaml.dll'. 
'Nightingale.exe' (Win32): Loaded 'C:\Program Files\WindowsApps\Microsoft.VCLibs.140.00.Debug_14.0.27508.1_x64__8wekyb3d8bbwe\vcruntime140_1_app.dll'. 
'Nightingale.exe' (Win32): Loaded 'C:\Program Files\WindowsApps\Microsoft.VCLibs.140.00.Debug_14.0.27508.1_x64__8wekyb3d8bbwe\msvcp140_app.dll'. 
'Nightingale.exe' (Win32): Loaded 'C:\Program Files\WindowsApps\Microsoft.UI.Xaml.2.1_2.11906.6001.0_x64__8wekyb3d8bbwe\Microsoft.UI.Xaml.dll'. 
'Nightingale.exe' (Win32): Loaded 'C:\Windows\System32\Windows.StateRepositoryPS.dll'. 
onecoreuap\base\mrt\runtime\com\dllsrv\mrtresourcemanager.cpp(228)\MrmCoreR.dll!00007FFF9973619E: (caller: 00007FFF8CD3FB48) ReturnHr(1) tid(341c) 80073B1F ResourceMap Not Found.
onecoreuap\base\mrt\runtime\com\dllsrv\mrtresourcemanager.cpp(228)\MrmCoreR.dll!00007FFF9973619E: (caller: 00007FFF8CD3FB48) ReturnHr(2) tid(341c) 80073B1F ResourceMap Not Found.
Exception thrown at 0x00007FFFA735A839 (KernelBase.dll) in Nightingale.exe: WinRT originate error - 0x80004005 : 'Cannot locate resource from 'ms-appx://Microsoft.UI.Xaml.2.2/Microsoft.UI.Xaml/Themes/19h1_themeresources.xaml'.'.
onecore\com\combase\winrt\error\restrictederror.cpp(1014)\combase.dll!00007FFFA89B50E0: (caller: 00007FFFA89AF19C) ReturnHr(1) tid(341c) 8007007E The specified module could not be found.
Exception thrown at 0x00007FFFA735A839 in Nightingale.exe: Microsoft C++ exception: winrt::hresult_error at memory location 0x0000008467CFDFD0.

The error seems to be: 'Cannot locate resource from 'ms-appx://Microsoft.UI.Xaml.2.2/Microsoft.UI.Xaml/Themes/19h1_themeresources.xaml'. Any ideas?

@dpaulino
Copy link
Contributor Author

Weird. I seem to have fixed it by cleaning the solution and then rebuilding.

The observable collection is being updated now. Going to do a few more tests before I close this issue.

@dpaulino
Copy link
Contributor Author

Looks good. Closing issue. Thanks for all your help.

@dpaulino
Copy link
Contributor Author

dpaulino commented Aug 21, 2019

@kaiguo I just found a crash and it's due to a requirement that I incorrectly stated at the start of this issue: groups cannot actually contain other groups.

The crash occurs when I have something looking like this:

  • group 1
  • group 2
    • leaf a

If I drag group 1 to hover in the space between group 2 and leaf a, a drop is allowed and it will lead to a crash for my app due to an invalid cast. In my scenario, the invalid cast is correct because groups cannot contain other groups. How could the sample you provided be modified to support this scenario?

@dpaulino dpaulino reopened this Aug 21, 2019
@dpaulino
Copy link
Contributor Author

@kaiguo friendly ping. Any thoughts on my previous comment?

@kaiguo
Copy link
Contributor

kaiguo commented Aug 26, 2019

We probably need some new APIs for this to get the drop position.

As a workaround for now, I think you can use InsertionPanel to get the item above/below the dropping position, which should help you figure out whether the drop is valid or not (e.g. block the drop if dragged item is a group, and the item above or its parent is a group). I put something here to show you how to get the above/below item by using InsertionPanel.

https://github.com/kaiguo/TreeViewConditionalReorderSample/blob/insertionpanel-test/TreeViewConditionalReorderSample/MyTreeViewItem.cs#L20

@dpaulino
Copy link
Contributor Author

I'll try this out and report back soon. I appreciate your help.

@dpaulino
Copy link
Contributor Author

@lukasf thanks for referencing that proposal. I would like to see this feature added ASAP though rather than wait for WinUI 3.0. My customers have been demanding proper drag and drop abilities for the past 4 months, and I cannot implement it properly due to this treeview limitation.

WinUI team, please consider addressing this as soon as possible.

@knightmeister
Copy link

knightmeister commented Oct 14, 2019

Just to throw my support onto this as well. I raised #381 a while ago. The drag and drop APIs feel very incomplete and inflexible.

Do we have an ETA on support for this?

It's something I'm hearing the need for a lot, as well.

@kaiguo
Copy link
Contributor

kaiguo commented Oct 14, 2019

Do we have an ETA on support for this?

I'm on some other lifted XAML work but I'll try and see if I can add something in 2.3 release (no guarantee😅).

@knightmeister
Copy link

knightmeister commented Oct 15, 2019

I'm on some other lifted XAML work but I'll try and see if I can add something in 2.3 release (no guarantee😅).

@kaiguo that would be awesome.

I think I have the same issue (in #381) as @dpaulino, I want to be able to enforce the order of items (I.e. have pinned items at the top of the tree view) and block moving certain items and make certain items not be drop targets.

Thanks!

@kaiguo
Copy link
Contributor

kaiguo commented Nov 20, 2019

@dpaulino I updated the sample app to use the latest TreeView changes, I think this should solve the issue you mentioned in this comment.

@dpaulino
Copy link
Contributor Author

Thanks for the update. Is this available in a prerelease package that I could try out?

@kaiguo
Copy link
Contributor

kaiguo commented Nov 22, 2019

Is this available in a prerelease package that I could try out?

Yeah, you can find the prerelease package in the sample app here.

@tzdlr
Copy link

tzdlr commented May 18, 2020

Small question on this topic. Is this change approved for stable releases? Wasnt able to work with the sample app provided on this topic.

@marcelwgn
Copy link
Collaborator

I think the change should be in the latest (pre)release version of WinUI.

@tzdlr
Copy link

tzdlr commented May 18, 2020

Well, the latest pre-release i found is 2.4.0-prerelease.200506001 where there is a 2.4.0 stable release. The topic was opened on an 2.3 and i'm still missing the "NewParent" property on TreeViewDragItemsCompletedEventArgs

@marcelwgn
Copy link
Collaborator

The "NewParent" property is still marked as preview, so it currently should only be available in prereleases.

@tzdlr
Copy link

tzdlr commented May 18, 2020

as said, i tested with several 2.4 pre releases, an wasn't able to access it

@marcelwgn
Copy link
Collaborator

It was renamed with #1692, the property is called "NewParentItem" now. Sorry about the confusion.

@tzdlr
Copy link

tzdlr commented May 18, 2020

Namingconversion are no problem in general... but, i cant find any. There are only two acessable properties, Items and DropResult. Call me dumb, but i'm not able to get a working solution.

@marcelwgn
Copy link
Collaborator

I've updated the linked project from @kaiguo to use the newest prerelease. Can you clone the following project and check if it compiles for you?

The updated sample: https://github.com/chingucoding/TreeViewConditionalReorderSample

@tzdlr
Copy link

tzdlr commented May 18, 2020

Yes, it does and works like a charme. Well, obivously i missed out my usings...
Both, Microsoft.UI.Xaml.Controls as Windows.UI.Xaml.Controls have an own CompletedEventArgs.... never saw this discrepancy -.- thanks for pushing me into right direction

@marcelwgn
Copy link
Collaborator

Glad to hear that we found the issue, happy that I could help you :)

@sjb-sjb
Copy link

sjb-sjb commented Dec 1, 2023

@chingucoding the link above is broken, can you say where the repo has gone?

@microsoft-github-policy-service microsoft-github-policy-service bot added the needs-triage Issue needs to be triaged by the area owners label Dec 1, 2023
@marcelwgn
Copy link
Collaborator

I'm afraid I've deleted as part of repo cleanups throughout the years @sjb-sjb. Is blocking dropping a TreeViewItem something you need guidance with?

@bpulliam bpulliam removed the needs-triage Issue needs to be triaged by the area owners label Dec 11, 2023
@sjb-sjb
Copy link

sjb-sjb commented Dec 31, 2023

@chingucoding Yes please, thanks for offering!

I understand the general point that one can control reordering locally by setting TreeView.CanReorderItems from within the OnDragEnter and other callbacks of TreeViewItem. I also see that one must set AllowDrop = true on all of the TreeViewItems in order to allow the callbacks to be invoked in the first place (and incidentally these callbacks replace PointerEntered / PointerExited). Also, if one does not want to allow reordering a dragged item so that it is outside of a given folder, then one must avoid setting TreeView.CanReorderItems if the drag pointer is above the folder rather than within or below the folder.

Overall the results are OK except for a visual problem. When an item moves to allow reordering, it can double up on the item that is being dragged. In the attached picture, L1 is being dragged down below L2. L2 has moved up to the spot where L1 previously was. But we can still see L1 behind L2. If we release the mouse button over G, then L1 is not dropped into G (this is correct) but L2 remains visually superimposed on L1. However when a refresh occurs then the two leaves L1, L2 appear in their correct places underneath F, first L1 then L2 below it.

Picture4

What I am not sure of is whether this is a bug or whether there is something additional that should be done in the overrides to prevent this. Thoughts?

I have a reference to the TreeView within my derived TreeViewItem and my item type is TreeEntity which contains flags CanDrop/CanReorder indicating the desired droppability / reorderability. The overrides are as follows:

    protected override void OnDragEnter(DragEventArgs args)
    {
        Debug.Assert(this.TreeView != null && this.TreeEntity != null);
        bool tooHigh = args.GetPosition(this).Y < (this.ActualHeight / 4.0);
        this.TreeView.CanReorderItems = this.TreeEntity.CanReorder && !tooHigh;
        Debug.WriteLine($"- DragEnter '{this.TreeEntity.Name}', CanDrop='{this.TreeEntity.CanDrop}', ParentCanDrop='{this.TreeEntity.CanReorder}' TreeViewer.CanReorderItems='{this.TreeView.CanReorderItems}'");
        if (!this.TreeEntity.CanDrop)
        {
            args.AcceptedOperation = DataPackageOperation.None;
            args.Handled = true;
            return;
        }
        base.OnDragEnter(args);
    }

    protected override void OnDragOver(DragEventArgs args)
    {
        Debug.Assert(this.TreeView != null && this.TreeEntity != null);
        bool tooHigh = args.GetPosition(this).Y < (this.ActualHeight / 4.0);
        this.TreeView.CanReorderItems = this.TreeEntity.CanReorder && !tooHigh;
        Debug.WriteLine($"- DragOver '{this.TreeEntity.Name}', CanDrop='{this.TreeEntity.CanDrop}', ParentCanDrop='{this.TreeEntity.CanReorder}' TreeViewer.CanReorderItems='{this.TreeView.CanReorderItems}'");
        if (!this.TreeEntity.CanDrop)
        {
            args.AcceptedOperation = DataPackageOperation.None;
            args.Handled = true;
            return;
        }
        base.OnDragOver(args);
    }

    protected override void OnDragLeave(DragEventArgs args)
    {
        Debug.Assert(this.TreeView != null && this.TreeEntity != null);
        bool tooHigh = args.GetPosition(this).Y < (this.ActualHeight / 4.0);
        this.TreeView.CanReorderItems = this.TreeEntity.CanReorder && !tooHigh;
        Debug.WriteLine($"- DragLeave '{this.TreeEntity.Name}', CanDrop='{this.TreeEntity.CanDrop}', ParentCanDrop='{this.TreeEntity.CanReorder}' TreeViewer.CanReorderItems='{this.TreeView.CanReorderItems}'");
        if (!this.TreeEntity.CanDrop)
        {
            args.AcceptedOperation = DataPackageOperation.None;
            args.Handled = true;
            return;
        }
        base.OnDragLeave(args);
    }

    protected override void OnDrop(DragEventArgs args)
    {
        Debug.Assert(this.TreeView != null && this.TreeEntity != null);
        Debug.WriteLine($"- DROP '{this.TreeEntity.Name}', CanDrop='{this.TreeEntity.CanDrop}', ParentCanDrop='{this.TreeEntity.CanReorder}' TreeViewer.CanReorderItems='{this.TreeView.CanReorderItems}'");
        if (!this.TreeEntity.CanDrop) {
            args.AcceptedOperation = DataPackageOperation.None;
            args.Handled = true;
            args.DataView.ReportOperationCompleted(DataPackageOperation.None);
            return;
        }
        base.OnDrop(args);
        Debug.WriteLine($"* DROP '{this.TreeEntity.Name}', CanDrop='{this.TreeEntity.CanDrop}', ParentCanDrop='{this.TreeEntity.CanReorder}' TreeViewer.CanReorderItems='{this.TreeView.CanReorderItems}'");
    }

I have attached a sample project using WinUI 3.
Testy.zip

@microsoft-github-policy-service microsoft-github-policy-service bot added the needs-triage Issue needs to be triaged by the area owners label Dec 31, 2023
@sjb-sjb
Copy link

sjb-sjb commented Jan 1, 2024

A less serious problem is that this approach does not always detect when a reorder is appropriate. Specifically in the scenario above, reordering can occur within the F subtree but not the G subtree. If we drag L1 down to G then TreeView.CanReorderItems is correctly set to false; then when we drag it back up into the blank space above G but below the superimposed L1/L2, it does not detect that a reorder can occur there. It will not re-enable TreeView.CanReorderItems until we drag it back up to the superimposed L1/L2, at which point a DragEnter in L2 is detected and the TreeView reordering is re-enabled. There does not seem to be any event that fires while dragging over the blank space after entering and leaving G. While we were dragging L1 down below L2, but before entering G, the ability to reorder below L2 was detected.

@marcelwgn
Copy link
Collaborator

Sorry for the delayed response @sjb-sjb. The project you shared seems fine, I also used the same approach back then (hence I haven't attached a new project showcasing that). My guess would be this is an issue with the control though the code you shared seems quite complicated. Is your intention to adjust where the position of items should be to reorder, hence the height calculations?

@codendone codendone removed the needs-triage Issue needs to be triaged by the area owners label Sep 27, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

10 participants