使用拖放重新排序后,按钮将丢失其命令绑定
本文关键字:按钮 绑定 命令 拖放 新排序 排序 | 更新日期: 2023-09-27 18:24:50
我创建了一个UserControl
,它将绑定到视图模型上动态创建的按钮列表,并允许我对它们进行拖放重新排序。
在我将按钮拖动到列表中的新位置后,我刚刚拖动的按钮在单击时将不再触发其命令。如果我移动列表中的另一个按钮,第一个按钮现在可以单击,而刚才拖动的按钮则不能。
命令窗口中没有绑定错误。
可以使用下面的代码和以下操作重现问题:
- 在HandleContentButton()上放置一个断点,当按钮被单击
- 单击按钮1,您将看到它触发了断点
- 将按钮1拖动到按钮2上,它们将切换位置
- 单击按钮1,不会发生任何事情
- 将按钮2拖到按钮3上
- 现在,当您单击按钮1时,它工作,但按钮2不工作
我的控件的XAML:
<UserControl x:Class="SafetyApplication.Controls.SortableItemsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SafetyApplication.Controls"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<ItemsControl Grid.Row="2" Grid.Column="0" IsTabStop="False" Name="icsp"
ItemsSource="{Binding Path=ContentButtons}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel x:Name="sp" AllowDrop="True"
PreviewMouseLeftButtonDown="sp_PreviewMouseLeftButtonDown"
PreviewMouseLeftButtonUp="sp_PreviewMouseLeftButtonUp"
PreviewMouseMove="sp_PreviewMouseMove"
DragEnter="sp_DragEnter" Drop="sp_Drop"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Grid>
</UserControl>
控件背后的代码:
public partial class SortableItemsControl : UserControl
{
#region Fields
private bool _isDown;
private bool _isDragging;
private Point _startPoint;
private UIElement _realDragSource;
private readonly UIElement _dummyDragSource = new UIElement();
#endregion
#region Constructor
public SortableItemsControl()
{
InitializeComponent();
}
#endregion
#region Dependency Property
#region Item Source
public static readonly DependencyProperty ItemSourceProperty =
DependencyProperty.Register("ItemSource", typeof(IEnumerable<UIElement>), typeof(SortableItemsControl),
new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnItemSourceChanged)));
public IEnumerable<UIElement> ItemSource
{
get { return this.GetValue(ItemSourceProperty) as IEnumerable<UIElement>; }
set { this.SetValue(ItemSourceProperty, value); }
}
private static void OnItemSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var sortableItemsControl = d as SortableItemsControl;
if (sortableItemsControl?.ItemSource != null)
{
sortableItemsControl.icsp.ItemsSource = sortableItemsControl.ItemSource;
sortableItemsControl.icsp.Items.Refresh();
}
}
#endregion
#endregion
#region Drag Drop Logic
private void sp_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var sp = FindChild<StackPanel>(Application.Current.MainWindow, "sp");
if (e.Source != sp)
{
_isDown = true;
_startPoint = e.GetPosition(sp);
}
}
private void sp_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
_isDown = false;
_isDragging = false;
if (_realDragSource != null) _realDragSource.ReleaseMouseCapture();
}
private void sp_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (_isDown)
{
var sp = FindChild<StackPanel>(Application.Current.MainWindow, "sp");
if ((_isDragging == false) &&
((Math.Abs(e.GetPosition(sp).X - _startPoint.X) > SystemParameters.MinimumHorizontalDragDistance) ||
(Math.Abs(e.GetPosition(sp).Y - _startPoint.Y) > SystemParameters.MinimumVerticalDragDistance)))
{
_isDragging = true;
_realDragSource = e.Source as UIElement;
if (_realDragSource != null) _realDragSource.CaptureMouse();
DragDrop.DoDragDrop(_dummyDragSource, new DataObject("UIElement", e.Source, true),
DragDropEffects.Move);
}
}
}
private void sp_DragEnter(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent("UIElement"))
{
e.Effects = DragDropEffects.Move;
}
}
private void sp_Drop(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent("UIElement"))
{
var sp = FindChild<StackPanel>(this, "sp");
UIElement droptarget = e.Source as UIElement;
int droptargetIndex = -1, i = 0;
foreach (UIElement element in sp.Children)
{
if (element.Equals(droptarget))
{
droptargetIndex = i;
break;
}
i++;
}
if (droptargetIndex != -1)
{
var elements = icsp.ItemsSource as IEnumerable<UIElement>;
if (elements != null)
{
var items = elements.ToList();
UIElement button = _realDragSource;
items.Remove(button);
items.Insert(droptargetIndex, button);
icsp.ItemsSource = items;
}
icsp.Items.Refresh();
}
_isDown = false;
_isDragging = false;
_realDragSource.ReleaseMouseCapture();
}
}
#endregion
#region Find Child
/// <summary>
/// Finds a Child of a given item in the visual tree.
/// </summary>
/// <param name="parent">A direct parent of the queried item.</param>
/// <typeparam name="T">The type of the queried item.</typeparam>
/// <param name="childName">x:Name or Name of child. </param>
/// <returns>The first parent item that matches the submitted type parameter.
/// If not matching item can be found,
/// a null parent is being returned.</returns>
public static T FindChild<T>(DependencyObject parent, string childName)
where T : DependencyObject
{
// Confirm parent and childName are valid.
if (parent == null) return null;
T foundChild = null;
int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < childrenCount; i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
// If the child is not of the request child type child
T childType = child as T;
if (childType == null)
{
// recursively drill down the tree
foundChild = FindChild<T>(child, childName);
// If the child is found, break so we do not overwrite the found child.
if (foundChild != null) break;
}
else if (!string.IsNullOrEmpty(childName))
{
var frameworkElement = child as FrameworkElement;
// If the child's name is set for search
if (frameworkElement != null && frameworkElement.Name == childName)
{
// if the child's name is of the request name
foundChild = (T)child;
break;
}
}
else
{
// child element found.
foundChild = (T)child;
break;
}
}
return foundChild;
}
#endregion
}
我的视图模型:
public class AdminViewModel
{
private readonly DataTransferManager _dataManager;
public AdminViewModel(DataTransferManager dataManager)
{
_dataManager = dataManager;
ContentButtonCommand = new RelayCommand<string>(HandleContentButton);
BuildContentTable();
}
public ICommand ContentButtonCommand { get; set; }
public IEnumerable<Button> ContentButtons { get; set; }
private void HandleContentButton(object buttonDefinition)
{
//do stuff
}
private void BuildContentTable()
{
List<Button> buttons = new List<Button>();
Button button1 = new Button();
button1.Content = "Test 1";
button1.Command = ContentButtonCommand;
button1.CommandParameter = "Test 1";
button1.Margin = new Thickness(5);
buttons.Add(button1);
Button button2 = new Button();
button2.Content = "Test 2";
button2.Command = ContentButtonCommand;
button2.CommandParameter = "Test 2";
button2.Margin = new Thickness(5);
buttons.Add(button2);
Button button3 = new Button();
button3.Content = "Test 3";
button3.Command = ContentButtonCommand;
button3.CommandParameter = "Test 3";
button3.Margin = new Thickness(5);
buttons.Add(button3);
ContentButtons = buttons;
}
}
一个退化的RelayCommand<T>
实现足以支持上述视图模型:
class RelayCommand<T> : ICommand
{
private readonly Action<T> _handler;
public RelayCommand(Action<T> handler)
{
_handler = handler;
}
public bool CanExecute(object parameter)
{
return true;
}
#pragma warning disable 0067
public event EventHandler CanExecuteChanged;
#pragma warning restore 0067
public void Execute(object parameter)
{
_handler((T)parameter);
}
}
并使用控制:
<controls:SortableItemsControl ItemSource="{Binding Path=ContentButtons}"/>
首先,非常感谢您提供的优秀代码示例。我只需要添加RelayCommand<T>
(见上面的编辑),考虑到它是一个常用模式的实现,很容易忘记它实际上不在WPF中,这是可以原谅的。(遗憾的是)很少有人能如此仔细地提出一个问题,我们对此深表感谢。
至于你的实际问题,有一个很容易解决的办法。在实际执行删除操作之后,将_realDragSource
设置为null
。例如,作为sp_Drop()
方法中的最后一件事,在调用ReleaseMouseCapture()
之后,包括以下语句:
_realDragSource = null;
这修复了代码中的问题,在该问题中,如果用户单击了您刚刚拖动的按钮,则sp_PreviewMouseLeftButtonUp()
方法会发现该字段为非null,并在处理鼠标向上按钮事件期间调用ReleaseMouseCapture()
。这导致Button
对象本身在处理鼠标事件之前停止观察鼠标事件
换句话说,Button
对象并没有失去与命令的绑定(事实上,您可以在调试器中检查它们,并看到它们的配置从未更改)。他们只是忽略了本来会执行命令的用户输入。