wpf 尽可能简单地在面板之间传输深度嵌套的对象

flvlnr44  于 2023-05-01  发布在  其他
关注(0)|答案(2)|浏览(124)

我试图创建一个简单的看板风格的应用程序在WPF,使用MVVM模式。
该板具有动态数量的列,每个列具有动态数量的任务。下面是它在 View 中的核心结构。

<Window.Content>
    <ScrollViewer>
        <ItemsControl ItemsSource="{Binding TaskColumns}">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <StackPanel/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <ItemsControl ItemsSource="{Binding}">
                        <ItemsControl.ItemsPanel>
                            <ItemsPanelTemplate>
                                <StackPanel Style="{StaticResource taskColumn}"/>
                            </ItemsPanelTemplate>
                        </ItemsControl.ItemsPanel>
                        <ItemsControl.ItemTemplate>
                            <DataTemplate>
                                <Border Style="{StaticResource task}">
                                    <TextBox Text="{Binding Text}"/>
                                </Border>
                            </DataTemplate>
                        </ItemsControl.ItemTemplate>
                    </ItemsControl>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </ScrollViewer>
</Window.Content>

此结构的 ViewModel 等价物是这些元素绑定到的ObservableCollection<ObservableCollection<Task>>。它们在 View 中的等价物是StackPanelBorder
我想使用拖放事件在两列之间传输任务(拖放边框-〉拖放堆栈面板),这本身很简单,但我不知道的是如何让ViewModel知道要传输哪些任务以及传输到哪个列。如果可能的话,可以在 View 中执行,并相应地更新 ViewModel
我试着从父母那里找到索引来传递它们:_viewModel.TransferTask(int sourceTaskIndex, int sourceColumnIndex, int targetColumnIndex),但是对于这样的结构,这个解决方案很快就陷入了一个过于复杂的混乱,有时是.Parent,有时是.TemplateParent(Panel)转换,对写和读都非常混乱。
我也不知道如何用其他方法得到这些指数。用canvas获取鼠标坐标似乎是一个核心选项,但似乎并不能解决复杂性问题。
一定有更好的办法吧?

pb3s4cty

pb3s4cty1#

您应该考虑将ItemsControl替换为ListBoxListBox是一个高级的ItemsControl,它允许滚动,并具有UI虚拟化(默认情况下启用)等性能功能。
创建一个专用的控件也是非常有意义的。例如KanbanBoard, Package 这些列并实现拖放逻辑。只是为了封装相关的逻辑。它还将有助于维护/扩展功能和重用控件。
你的设计太复杂了,为了更方便地实现拖放功能,应该进行更改。
下一步是简化数据结构。看板板反映了一个工作流,而每一列代表了一个活动。数据结构没有理由已经反映了这些列。它应该是一个简单的平面项目集合。
将项视为“物理”数据实体,而列只是抽象或抽象Map,如通过工作流转换的数据项的索引/状态。
因此,简单工作项数据模型必须具有Map到其当前工作流状态(列)的状态或索引,而不是具有反映工作流本身的数据结构。现在,当工作项根据特定的转换规则从工作流状态转换到工作流状态时,只需修改数据模型的属性以反映新状态。
例如,当转换到最后一列时,工作项的状态会更改e。例如,从“查看”到“关闭”。它仍然保留在最初的集合中。

下面的示例代码是一种伪代码,以给予类设计和类责任的概念。

这意味着您将数据与可视化分离。
客户端(e。例如视图模型类或数据模型)。..
1.将工作项集合绑定到类型为IListKanbanBoard.ItemsSource属性。KanbanBoard不知道集合的数据类型。这是数据部分。
1.使用KanbaBoard公开的API定义列。列由索引和名称定义。
API可以是另一个IList类型的KanbanBoard.ColumnSource集合属性,允许绑定另一个e。例如string集合。每个string表示一个列名,string的索引Map到列的索引(可选地,KanbanBoard可以允许定义分配给ColumnTemplate属性的DataTemplate,该属性支持列定义的任何数据类型)。这意味着重新排列列源集合将重新排列KanbanBoard中的列(如果源集合是ObservableCollection)。
1.通过拖放转换一个项目。转换将更改工作项的列索引。
1.可以定义E。例如,WorkflowItem.Index属性(假定源集合包含一组WorkflowItem数据模型),该属性通过KanbaBoard.ItemContainerStyle属性绑定到项容器。这需要KanbaBoard引入一个自定义项目容器类型KanbanBoardItem,该类型可以扩展ListBoxItem,并添加一个ColumnIndex属性以启用WorkflowIndex的模型绑定。
现在,当一个项目转换时,KanbanBoard将相应地调整KanbaBoardItem.ColumnIndex属性。始终在容器上工作,而不是在数据项上工作是很重要的。
因此,要知道工作项在工作流中的位置,您必须阅读e。例如WorkflowItem.Index性质。您可以添加其他信息,例如添加State属性等。KanbaBoardItem(和数据模型)。

KanbanBoard.cs

// Pseudo code class
class KanbanBoard : Control
{
  /* Dependenciy properties */
  public IList ItemsSource { get; set; }
  public Style ItemContainerStyle { get; set; }
  public StyleSelector ItemContainerStyleSelector { get; set }
  public DataTemplate ItemTemplate { get; set; }
  public IList ColumnSource { get; set; }

  // Style for all columns (target KanbanBoardColumn)
  public Style ColumnStyle { get; set; }

  // StyleSelector to apply individual styling for each column (target KanbanBoardColumn)
  public StyleSelector ColumnStyleSelector { get; set }

  protected override void OnApplyTemplate()
  {
    var PART_ColumnHost = (ListBox)GetTemplateChild("PART_ColumnHost");
    PART_ColumnHost.ItemsSource = this.ColumnSource;
    for (int columnIndex = 0; columnIndex < this.ColumnSource.Count; columnIndex++)
    {
      object columnItem = this.ColumnSource[columnIndex];
      var columnItemContainer = (KanbanBoardColumn)PART_ColumnHost.ItemContainerGenerator.ContainerFromItem(columnItem);
      var columnStyle = this.ColumnStyleSelector?.SelectStyle(columnItem, columnItemContainer)
        ?? this.ColumnStyle;

      // The item container is a KanbanBoardColumn 
      // which is an extended ListBox (see definition below)
      columnItemContainer.Style = columnStyle;
      columnItemContainer.ItemContainerStyle = this.ItemContainerStyle;
      columnItemContainer.ItemContainerStyleSelector = this.ItemContainerStyleSelector;
      columnItemContainer.ItemTemplate = this.ItemTemplate;
      columnItemContainer.ColumnIndex = columnIndex;
      
      // Initialize first column with work items
      if (columnIndex == 0)
      {
        columnItemContainer.LoadItems(this.ItemsSource);
      }
    }
  }
}

KanbanBoardItem。cs

// Pseudo code class
class KanbanBoardItem : ListBoxItem
{
  /* Dependenciy properties */
  public int ColumnIndex { get; set; }
  public Status Status { get; set; }
}

状态.cs

// Pseudo code class
enum Status
{
  Inactive = 0,
  Open,
  Closed
}

既然已经定义了KanbanBoard API,我们就需要设计其内部结构。
1.要显示列,我们可以使用ListBox控件。为了使用自定义的KanbanBoardItem容器,我们需要扩展ListBox以覆盖ItemsControl.GetContainerForItemOverrideItemsControl.PrepareContainerForItemOverride方法。ItemsControl.GetContainerForItemOverride只是返回我们的定制KanbanBoardItem,然后由扩展的ListBox使用。扩展的ListBox,例如KanbanBoardColumn,负责处理项目删除。如果一个项目被删除,它将设置KanbanBoardItem.Index属性。由于附加的特性,比如header,修改ListBox的默认Style也是有意义的。
1.我们添加一个LsitBox´ to the ControlTemplate of the KanbanBoard . This ListBox is used to dynamically create a KanbanBoardColumn controls based on the KanbanBoard。ColumnSource collection property. The client code can define an optional [ StyleSelector] [1](https://learn.microsoft.com/en-us/dotnet/api/system.windows.controls.styleselector?view=windowsdesktop-8.0),以便单独自定义列(例如例如,对工作流状态着色)。 1.我们将KanbanBoard.ItemContainerStyle分配给每个KanbanBoardColumn.ItemContainerStyle属性(参见上面的KanbanBorad类伪代码)。 1.我们将KanbaBoard.ItemTemplate分配给每个KanbanBoardColumn.Itemtemplate属性(参见上面的KanbanBorad类伪代码)。 1.拖放在KanbanBoardColumn中实现。它会在拖动开始时移除项目,并在拖放时插入项目,或者在拖动取消时重新插入项目。KanbanBoardItem is the data payload of the drag&drop event data. On drop it also sets the ´KanbanBoardItem.ColumnIndex完成转换。

KanbaBoardColumn。cs

// Pseudo code class
class KanbanBoardColumn : ListBox
{
  /* Dependenciy properties */
  public object ColumnHeader { get; set; }
  public int ColumnIndex { get; set; }
  private ItemsInternal { get; set; }

  public KanbanBoradColumn()
  {
    this.Items = new ObservableCollection<object>();
    this.ItemsSource = this.ItemsInternal;
  }

  public void LoadItems(IEnumerable newItems)
  {
    this.ItemsInternal = new ObservableCollection<object>(newItems);
    this.ItemsSource = this.ItemsInternal;
  }
 
  protected override void GetContainerForItemOverride()
    => new KanbanBoardItem();
 
  protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
  {
    base.PrepareContainerForItemOverride(element, item);
    if (element is KanbanBoardItem kanbanBoardItem)
    {
      kanbanBoardItem.Index = this.ColumnIndex;
    }
  }

  protected override OnDrop(DragEventArgs e)
  {
    var droppedItemContainer = e.Data.GetData(typeof(KanbanBoardItem)) as KanbanBoardItem;
    var item = droppedItemContainer.Content;
    this.ItemsInternal.Add(item);
    var generatedItemContainer = this.ItemContaionerGenerator.ContainerFromItem(item) as KanbanBoardItem;
    generatedItemContainer.ColumnIndex = this.ColumnIndex;
  }

  protected override OnMouseLeftButtonDown(MouseEventArgs e)
  {
    object clickedItem = GetClickedItem();
    this.ItemsInternal.Remmove(clickedItem);
    var clickedItemContainer = this.ItemContaionerGenerator.ContainerFromItem(clickedItem) as KanbanBoardItem;
   
    // TODO:: Start drag operation and use the clickedItemContainer as payload.
    // If cancelled re-insert the item back into the ItemsInternal collection.
  }   
}

KanbanBoardColumnStyle。xaml

<Style TargetType="KanbanBoardColumn">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="KanbanBoradColumn">
        <StackPanel>
          <TextBlock Text="{TemplateBinding Columnheader}" />
          
          <ScrollViewer CanContentScroll="True">
            <ItemsPresenter />
          </ScrollViewer>
        </StackPanel>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

KanbanBoardStyle。xaml

<Style TargetType="KanbanBoard">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="KanbanBorad">
        <listBox x:Name="PART_ColumnHost" />
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

使用示例

MainViewModel。cs

class MainViewModel : INotifyPropertyChanged
{
  public ObservableCollection<WorkItem> WorkItems { get; }
  public ObservableCollection<string> ColumnItems { get; }
}

主窗口。xaml

<Window>
  <Window.DataContext>
    <MainViewModel />
  </Window.DataContext>

  <KanbanBoard ItemsSource="{Binding WorkItems}"
               ColumnSource="{Binding ColumnItems}">
    <KanbanBoard.ItemContainerStyle>
      <Style TargetType="KanbanBoardItem">

        <!-- Connect item container to item 
             to transfer data like column index -->
        <Setter Property="ColumnIndex" Value="{Binding Index, Mode=TwoWay}" />
      </Style>
    </KanbanBoard.ItemContainerStyle>
  </KanbanBoard>
</Window>
kjthegm6

kjthegm62#

使用FrameworkElement.Tag属性的简单解决方案:

Tag="{Binding}"添加到每个上下文元素允许我们通过上下文搜索必要的元素,并允许我们访问相应的ViewModel对象。
VisualTreeHelper.GetParent()消除了.Parent/.TemplateParent的困境。

查看:

private void TaskColumn_Drop(object sender, DragEventArgs e)
{
    var dropedElement = (Border)e.Data.GetData(typeof(Border));

    var task = (Task)dropedElement.Tag;
    var sourceColumn = (ObservableCollection<Task>)FindTaggedParent(dropedElement).Tag;
    var targetColumn = (ObservableCollection<Task>)((StackPanel)sender).Tag;

    _viewModel.TransferTask(task, sourceColumn, targetColumn);
}

ViewModel:

public void TransferTask(Task task, ObservableCollection<Task> sourceColumn, ObservableCollection<Task> targetColumn)
{
    sourceColumn.Remove(task);
    targetColumn.Add(task);
}

下面我写了一个递归搜索必要父对象的方法。在这种情况下,它会查找所有不是 null 的标签,但是如果需要,可以用更具体的条件来替换。

// Throws NullReferenceException on failure.
private FrameworkElement FindTaggedParent(FrameworkElement element)
{
    var parent = (FrameworkElement)VisualTreeHelper.GetParent(element);

    if (parent.Tag != null)
    {
        return parent;
    }
    return FindTaggedParent(parent);
}

相关问题