views:

63

answers:

4

Hi,

I'm writing a custom ItemsControl (a tabbed document container), where each item (tab) can remove itself from the UI when the user closes it. However, I can't remove it directly from the ItemsControl.Items collection, because the items can be databound. So I have to remove it from the ItemsSource, which can be anything (ICollection, DataTable, DataSourceProvider...).

In the context of my application, I know what the actual type of the ItemsSource will be, but I want that control to be more generic so that I can reuse it later.

So I'm looking for a way to remove an item from a data source, without knowing its type. I could use reflection, but it feels dirty... So far the best solution I came up with is using dynamic :

    internal void CloseTab(TabDocumentContainerItem tabDocumentContainerItem)
    {
        // TODO prompt user for confirmation (CancelEventHandler ?)

        var item = ItemContainerGenerator.ItemFromContainer(tabDocumentContainerItem);

        // TODO find a better way...
        try
        {
            dynamic items = ItemsSource;
            dynamic it = item;
            items.Remove(it);
        }
        catch(RuntimeBinderException ex)
        {
            Trace.TraceError("Oops... " + ex.ToString());
        }
    }

But I'm not really happy with it, I'm sure there must be a better way. Any suggestions would be appreciated !

A: 

Design practice dictates that you should really know what your ItemsSource is and be able to remove it from there directly. The binding then automatically updates the view of course.

However, if you are absolutely intent on some sort of generic functionality for removal, casting your ItemsSource to ICollection or ICollection<T> and then calling Remove sounds like a better/more reliable way to go than using the dynamic features of .NET.

Noldorin
Actually, good design would ensure the `ItemsControl` has no knowledge of the types in the `ItemsSource` at all.
Kent Boogaart
I would agree if I had full control over that ItemsSource property... but I don't. It's part of WPF, is of type `object`, and can be anything that supports enumeration. And my control is designed to accept anything, I don't want to limit myself to a specific type
Thomas Levesque
Down-vote why??
Noldorin
@Thomas: What's wrong with the solution of my 2nd paragraph btw?
Noldorin
Nothing's wrong, I'm not the one who voted you down... About your 2nd paragraph, unfortunately I can't do that, because I don't know the actual item type, and `ICollection<T>` doesn't inherit from `ICollection`
Thomas Levesque
A: 

As you've found, your ItemsControl doesn't have intrinsic knowledge of the items being bound - those types are provided by consumers of your control. And you can't modify the collection directly because it may be data-bound.

The trick is to ensure each item is wrapped by a custom class (item container) of your choosing. Your ItemsControl you can provide this in the GetContainerForItemOverride method.

From there, you can define properties on your custom item container that you then bind to in your default template. For example, you could have a property called State that changes between Docked, Floating, and Closed. Your template would use this property to determine how - and whether - to show the item.

So you will not actually be changing the underlying data source at all. Instead, you will change a control-specific layer on top of the underlying data items that give you the info you need to implement your control.

HTH,
Kent

Kent Boogaart
Kent, thanks for your answer. I have already created a custom container and overriden GetContainerForItemOverride. But the fact is I really want to remove the item from the underlying collection, I don't want to just hide it. Perhaps I should just delegate the implementation of "close" to the user, by handling an event (code-behind) or binding a command (ViewModel)
Thomas Levesque
@Thomas: no problem. Can you explain why it is you need to remove the item? Perhaps a filtered collection view would suffice? Or perhaps your item should execute a command when the user closes it, and the removal is up to the consuming code. Seems to me your desire to create a generic control can't work if you do the removal yourself.
Kent Boogaart
The control displays a list of open documents (SQL worksheets in that case). It is bound to a list of worksheets exposed by a ViewModel. When I click the close button on a tab (part of the template of the TabDocumentContainerItem), the container calls CloseTab on its parent (the TabDocumentContainer), which removes the document from the worksheet collection. Actually I found a solution, I'll post it in a few minutes
Thomas Levesque
+1  A: 

The ItemCollection returned by ItemsControl.Items won't allow you to call Remove directly, but it implements IEditableCollectionView and does allow you to call the Remove method in that interface.

This will only work if the collection view bound to ItemsSource implements IEditableCollectionView itself. The default collection view will for most mutable collections, although not for objects that implement ICollection but not IList.

IEditableCollectionView items = tabControl.Items; //Cast to interface
if (items.CanRemove)
{
    items.Remove(tabControl.SelectedItem);
}
Quartermeister
Interesting answer, I didn't know about that interface. However I think I will stick to the solution I found (see my answer), because it's more adequate in my case. Also, if the ItemsSource is a IEnumerable, CanRemove will return false, but the ViewModel might have access to the actual collection and be able to remove the item.
Thomas Levesque
A: 

OK, I found a solution...

  • If the ItemsSource is databound, I either raise an event (for use with code-behind) or invoke a command (for use with a ViewModel) to remove the item from the ItemsSource collection.

  • If it's not databound, I raise an event to prompt the user for confirmation, and I remove the container directly from Items

    public static readonly DependencyProperty CloseTabCommandProperty =
        DependencyProperty.Register(
            "CloseTabCommand",
            typeof(ICommand),
            typeof(TabDocumentContainer),
            new UIPropertyMetadata(null));
    
    
    public ICommand CloseTabCommand
    {
        get { return (ICommand)GetValue(CloseTabCommandProperty); }
        set { SetValue(CloseTabCommandProperty, value); }
    }
    
    
    public event EventHandler<RequestCloseTabEventArgs> RequestCloseTab;
    public event EventHandler<TabClosingEventArgs> TabClosing;
    
    
    internal void CloseTab(TabDocumentContainerItem tabDocumentContainerItem)
    {
        if (ItemsSource != null) // Databound
        {
            object item = ItemContainerGenerator.ItemFromContainer(tabDocumentContainerItem);
            if (item == null || item == DependencyProperty.UnsetValue)
            {
                return;
            }
            if (RequestCloseTab != null)
            {
                var args = new RequestCloseTabEventArgs(item);
                RequestCloseTab(this, args);
            }
            else if (CloseTabCommand != null)
            {
                if (CloseTabCommand.CanExecute(item))
                {
                    CloseTabCommand.Execute(item);
                }
            }
        }
        else // Not databound
        {
            if (TabClosing != null)
            {
                var args = new TabClosingEventArgs(tabDocumentContainerItem);
                TabClosing(this, args);
                if (args.Cancel)
                    return;
            }
            Items.Remove(tabDocumentContainerItem);
        }
    }
    
Thomas Levesque