WPF - 在UserControl中绑定一个ObservableCollection依赖属性

7

我有一个控件

class DragGrid : Grid { ... }

继承自原始网格并使其子元素可拖动和调整大小。我需要将名为WorkItemsProperty的自定义DP绑定到类型为WorkItem(实现INotifyPropertyChanged)的可观察集合。网格中的每个元素都绑定到集合项。

每当用户在运行时动态添加新项(无法在XAML中声明项!)或从该集合中删除项时,DragGrid上的WorkItems DP应更新,并且网格中的子级(其中每个子级表示WorkItem集合项)。

我的问题是,DP如何通知控件哪个网格中的子元素必须被删除更改(“更改”意味着用户拖动了一个元素或使用鼠标调整了它的大小)或添加,以及如何确定哪个现有子元素是需要删除或更改的那个。 我理解这就是DependencyPropertyChangedCallback发挥作用的地方。但是它只在设置DP属性时调用,而不是在集合中的某些内容更改(例如添加、删除项)时调用。所以最终,DragGrid控件是否需要订阅CollectionChanged事件?我应该在什么时候挂钩事件处理程序呢?

*编辑: 首先使用网格的原因是因为我希望能够保持用户拖动或调整网格中控件的最小增量。一个控件代表一个时间段,每个网格列表示15分钟(这是最小值)。在Canvas中使用Thumbs实现这一点很困难且容易出错。实现DragGrid解决了我的用户交互问题。而且,Canvas不可扩展,所以时间跨度必须一直重新计算。使用网格,我没有这个问题,因为列告诉我时间,无论大小如何。

2个回答

17

针对你实际的问题:

你应该像你提到的那样添加一个DependencyPropertyChanged处理程序。在这个处理程序中,你应该向新集合的CollectionChanged属性添加一个事件处理程序,并从旧集合中移除该处理程序,就像这样:

    public ObservableCollection<WorkItem> WorkItems
    {
        get { return (ObservableCollection<WorkItem>)GetValue(WorkItemsProperty); }
        set { SetValue(WorkItemsProperty, value); }
    }

    // Using a DependencyProperty as the backing store for WorkItems.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty WorkItemsProperty =
        DependencyProperty.Register("WorkItems", typeof(ObservableCollection<WorkItem>), typeof(DragGrid), new FrameworkPropertyMetadata(null, OnWorkItemsChanged));

    private static void OnWorkItemsChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        DragGrid me = sender as DragGrid;

        var old = e.OldValue as ObservableCollection<WorkItem>;

        if (old != null)
            old.CollectionChanged -= me.OnWorkCollectionChanged;

        var n = e.NewValue as ObservableCollection<WorkItem>;

        if (n != null)
            n.CollectionChanged += me.OnWorkCollectionChanged;
    }

    private void OnWorkCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == NotifyCollectionChangedAction.Reset)
        {
            // Clear and update entire collection
        }

        if (e.NewItems != null)
        {
            foreach (WorkItem item in e.NewItems)
            {
                // Subscribe for changes on item
                item.PropertyChanged += OnWorkItemChanged;

                // Add item to internal collection
            }
        }

        if (e.OldItems != null)
        {
            foreach (WorkItem item in e.OldItems)
            {
                // Unsubscribe for changes on item
                item.PropertyChanged -= OnWorkItemChanged;

                // Remove item from internal collection
            }
        }
    }

    private void OnWorkItemChanged(object sender, PropertyChangedEventArgs e)
    {
        // Modify existing item in internal collection
    }

正如gehho所解释的那样,你似乎没有按照原来的意图使用Grid类,尽管你可能已经开发了很长时间,不想在这个时候重新开始。从Panel派生的类实际上只打算用于可视化地绘制/排列它们的子元素,而不是对它们进行操作和增强。查看ItemsControlWPF内容模型以获取更多信息。


Josh,谢谢你。在CollectionChanged事件中,我无法向workitems的内部集合添加内容,因为.NET不允许这样做。我想你的意思是将控件添加到网格的Children集合中。 - John
1
不,实际上我是指将它添加到您的集合中+网格的子项集合或其他集合中。我不确定您的控件如何工作,但显然您正在管理某些项目的集合。根据您的评论,听起来您正在管理Grid.Children集合,所以是的,请将其添加在那里。 - Josh G
理想情况下,您可以使用ItemsControl(或可能是其自定义子类,如果没有更好地了解您的问题,则不确定),并将WorkItems设置为ItemsControl的子项。然后,您将使用ItemsPanelTemplate在网格上显示项目。您将使用DataTemplate生成工作项的实际可视化效果。在项模板控件、容器控件(由ItemsControl生成)和网格上进行数据绑定,以便正确放置项目。 - Josh G
谢谢Josh,你的建议解决了我们的问题。它按预期工作! - John

1

抱歉,我没有解决您具体自定义Grid问题的方案,但我只有一个建议,让您更容易地完成它(并且,我认为这是WPF设计师的意图)。实际上,Grid不是用来排列items的控件。它是一个排列ControlsPanel。所以,我想这就是您的解决方案出现问题的原因之一。

相反,我会使用ItemsControl(例如ListBox),并将ItemsPanel设置为Canvas

<ListBox ItemsSource="{Binding WorkItemsProperty}">
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas IsItemsHost="True"/>
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
</ListBox>

现在,您可以在您的WorkItem(或WorkItemViewModel)类中定义适当的属性,例如XPosYPos,这些属性将与Canvas.LeftCanvas.Top属性进行数据绑定,如下所示:

<Style x:Key="WorkItemStyle" TargetType="{x:Type ListBoxItem}">
    <Setter Property="Canvas.Left" Value="{Binding XPos, Mode=TwoWay}"/>
    <Setter Property="Canvas.Top" Value="{Binding YPos, Mode=TwoWay}"/>
</Style>

然后,您可以通过将ListBoxItemContainerStyle属性分配为此项样式来使用它:

ItemContainerStyle="{StaticResource WorkItemStyle}"

我不知道如何实现拖放功能,因为我从未尝试过,但是显然你已经在自定义的Grid中实现了它,所以在ListBox中使用它应该不是什么大问题。但是,如果你更新了WorkItem的属性,它应该会自动重新定位元素。此外,如果你向集合(WorkItemsProperty)中添加/删除项目,它也会自动添加/删除,因为ListBox与集合进行了数据绑定。

根据你的情况,你可能需要更改WorkItemStyle。例如,如果ListBox在运行时调整大小,你可能需要使位置相对于容器(Canvas)的大小。因此,你需要使用MultiBinding而不是简单的Binding。但这是另一个故事了...

现在,你可以决定是否采用这种方法,或者你的Grid已经快完成了,你不想再做改变。我知道这很难,但在我看来,上述方法是更清晰(和更容易!)的一种!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接