1

Given:

class StringRecord : INotifyPropertyChanged
{
    public string Key   { get; set; } // real INPC implementation is omitted
    public string Value { get; set; } // real INPC implementation is omitted
    ...
}

class Container
{
    public ObservableKeyedCollection<string, StringRecord> Params { get; set; }
    ...
{

The ObservableKeyedCollection is the one found here.

A TextBox is bound to one of the collection items (DataContext is inherited):

<TextBox Text="{Binding Params[APN_HOST].Value}"/>

When I manually add the "APN_HOST" item to the collection, the binding works as expected.

Now, where I'm stuck: I want to be able to edit an empty collection that way, i.e.,

If there's no item in the collection with the specified key, and user types some text to the textbox, it would result in a new item added to the collection with the corresponding key.

I tried to implement some kind of "default if not found" semantics in the collection, but it resulted in all the textboxes being bound to the same default instance of StringRecord, sharing a single value :)

Feels like I'm overlooking something really obvious here.

vines
  • 5,160
  • 1
  • 27
  • 49
  • Have you tried [BindingBase.FallbackValue](http://msdn.microsoft.com/en-us/library/system.windows.data.bindingbase.fallbackvalue) or [BindingBase.TargetNullValue](http://msdn.microsoft.com/en-us/library/system.windows.data.bindingbase.targetnullvalue)? – LPL Jun 08 '12 at 19:37
  • @LPL Yeah, it shows up in the textbox expectedly and correctly, but when edited, new item still doesn't get added to the collection. – vines Jun 08 '12 at 20:26

1 Answers1

0

Well, that was a really tough one!

What I've done is:

1) Extended ObservableKeyedCollection with

public class ObservableKeyedCollection<TKey, TItem> : KeyedCollection<TKey, TItem>, INotifyCollectionChanged
{
    ...
    Func<TKey,TItem> m_newItemDelegate;

    public ObservableKeyedCollection(Func<TItem, TKey> getKeyForItemDelegate, Func<TKey, TItem> newItemDelegate = null)
        : base()
    {
        ...
        m_newItemDelegate = newItemDelegate;
    }

    public new TItem this[TKey key]
    {
        get
        {
            if (m_newItemDelegate != null && !Contains(key))
            {
                TItem i = m_newItemDelegate(key);
                var i_as_inpc = i as INotifyPropertyChanged;
                if (i_as_inpc != null)
                    i_as_inpc.PropertyChanged += new PropertyChangedEventHandler(AddItemOnChangeHandler);
                else
                    Add(i);
                return i;
            }
            return base[key];
        }
        set
        {
            if (Contains(key)) Remove(key);
            Add(value);
        }
    }

    private void AddItemOnChangeHandler(object sender, PropertyChangedEventArgs e)
    {
        (sender as INotifyPropertyChanged).PropertyChanged -= AddItemOnChangeHandler;
        Add((TItem)sender);
    }
  • Despite that the indexer is declared as new, binding engine correctly resolves it.
  • If the new-item-delegate is provided, we utilise it for obtaining some default item instance for a missing key. This new item is not immediately added to the collection, because it could violate the requirement of only adding values after user has actually edited them. Instead,
  • If the item implements INotifyPropertyChanged, we hook to its PropertyChanged event, add the item to the collection from the handler and immediately unsubscribe from PropertyChanged.

This alone would be enough, but... for some strange reason, nested property notifications doesn't fire!

2) Rewritten the binding as follows:

<TextBox DataContext="{Binding Params[APN_HOST]}" Text="{Binding Value}" />

Now it fires, triggering the above mechanism!

EDIT. After some discussion conducted here, I now see that the second modification was inevitable part of the solution, because of the defaulting behaviour. There's simply no other possibility to determine which instance has been changed.

Community
  • 1
  • 1
vines
  • 5,160
  • 1
  • 27
  • 49