views:

834

answers:

2

I have a control

class DragGrid : Grid { ... }

which inherits from the original grid and enables dragging and resizing its child elements. I need to bind a custom DP named WorkItemsProperty to an observable collection of type WorkItem (implements INotifyPropertyChanged). Each element in the grid is bound to a collection item.

Whenever the user adds a new item dynamically at runtime (the items cannot be declared in XAML!), or deletes an item from that collection, the WorkItems DP on the DragGrid should be updated, and the children in the grid (where each child represents a WorkItem collection item).

My question is how does the DP notify the control about which child element in the grid must be removed, changed ('change' means user dragged an element, or resized it with the mouse) or added, and how would I identify which one of the existing children is the one that needs to be deleted or changed. I understand that this is where the DependencyPropertyChangedCallback comes in. But that only gets called when the DP property is set anew, not when something inside the collection changes (like add, remove item). So in the end, does the DragGrid control somehow need to subscribe to the CollectionChanged event? At what point would I hook up the event handler for that?

EDIT:: The reason for using a Grid in the first place is because I want to be able to maintain a minimum delta for when the user drags or resizes the control in the Grid. A control represents a time span, and each grid column represents 15min (which is the minimum value). Doing this in a Canvas with Thumbs was difficult and buggy. Implementing a DragGrid solved my user interaction problems. Also, a Canvas is not scalable, so the time spans would have to recalculated all the time. With the Grid, I don't have the problem because the columns tell me the time no matter the size.*

A: 

Sorry, I have no solution to your concrete custom Grid problem, but I only have a suggestion how you could do it easier (and, I suppose, how it is meant by the WPF designers). Actually, a Grid is not a control to arrange items. It is a Panel which arranges Controls. So, I suppose, this is (one of) the reason(s) why you are getting into trouble with your solution.

What I would use instead, is an ItemsControl (e.g. a ListBox) with a Canvas as ItemsPanel.

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

Now, you define appropriate properties in your WorkItem (or WorkItemViewModel) class like XPos and YPos which will be databound to the Canvas.Left and Canvas.Top properties like this:

<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>

You can then use this item style by assigning the ItemContainerStyle property of the ListBox:

ItemContainerStyle="{StaticResource WorkItemStyle}"

I do not know how to implement the drag and drop stuff because I have never done that, but obviously you have already done it for your custom Grid, so it should not be a big problem to use it in a ListBox as well. However, if you update the properties of your WorkItem, it should automatically reposition the element. Also, if you add/remove an item to/from your collection (WorkItemsProperty), it will automatically be added/removed because the ListBox is data-bound to the collection.

You might have to change your WorkItemStyle depending on your scenario. For example, if the ListBox is resized at runtime, you might have to make the positions relative to the container's (Canvas') size. Therefore, you would need a MultiBinding instead of the simple Binding. But that's another story...

Now, it is your decision whether you can still move to this approach or whether your Grid is almost finished and you are not willing to change. I know it is hard, but in my eyes, the above approach is the cleaner (and easier!) one!

gehho
A: 

In answer to your actual question:

You should add a DepencyPropertyChanged handler, as you mentioned. In this handler, you should add an event handler to the CollectionChanged property on the new collection and remove the handler from the old collection, like this:

    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
    }

As gehho explained, it sounds like you are not using the Grid class as originally intended, although you may be too far into development to want to start over at this point. Classes derived from Panel are actually intended only to visually draw/arrange their children, rather than manipulating and enhancing them. Check out ItemsControl and the WPF Content Model to learn more.

Josh G
Josh, thanks for this. In the collectionchanged event, I cannot add to the internal collection of workitems, because .NET doesn't allow that. I think what you meant here is to add the controls to the grid's Children collection.
John
No, actually I meant add it to YOUR collection + the grid's Children collection or etc. I am not sure exactly how your control works, but it is obvious that you are doing work to manage some collection of items. Based on your comment, it sounds like you are managing the Grid.Children collection, so yes, add it there.
Josh G
Ideally you would take an ItemsControl (or possibly a custom child class from it, not sure without better understanding your problem), and you would set the WorkItems as the children of the ItemsControl.You would then use an ItemsPanelTemplate to display the items on a Grid. You would use a DataTemplate to generate the actual visuals for the work items. DataBinding on the item template control , the container control (generated by the ItemsControl), and the Grid would allow items to be drawn in their correct places.
Josh G
Thanks Josh, your suggestion solved our problem. It works as intended!
John