Search code examples
wpfdictionarydata-bindingobservable

Databinding ObservableDictionary to DataGrid not working


*I'm using a class ObservableDictionary to try and display Dictionary items in a datagrid that will be observable. I am simply try to databind the object but the grid is coming up blank.

I added the datacontext but still get no data displayed. I see postings for the ObservableCollections but no samples for ObservableDictionary.

Here is xaml:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApplication1"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="725">
    <Grid>
        <DataGrid Name ="testGrid" ItemsSource="{Binding Categories, Mode=OneWay}" Margin="21,22,370,50" AutoGenerateColumns="False">
           <DataGrid.Columns>
                <DataGridTextColumn  Header="Name" Width="220" Binding="{Binding Keys}" >
                    <DataGridTextColumn.ElementStyle>
                        <Style TargetType="TextBlock">
                            <Setter Property="TextWrapping" Value="Wrap"/>
                        </Style>
                    </DataGridTextColumn.ElementStyle>
                </DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>

    </Grid>
</Window>

Here is the MainWindow.xaml.cs:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows;

namespace WpfApplication1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public ObservableDictionary<string, string> _categories = new ObservableDictionary<string, string>();
        public MainWindow()
        {

            DataContext = this;

            InitializeComponent();

            _categories.Add("Island", "IS");
            _categories.Add("Land", "LD");
            _categories.Add("Sea", "SE");
        }

        public ObservableDictionary<string, string> Categories
        {
            get
            {
                return _categories;
            }
        }

}

And finally here is the ObservableDictionary.cs:

    using System.Linq;
    using System.ComponentModel;
    using System.Collections.Generic;
    using System.Collections.Specialized;

    namespace System.Collections.ObjectModel
    {
        public class ObservableDictionary<TKey, TValue> : IDictionary<TKey, TValue>, INotifyCollectionChanged, INotifyPropertyChanged
        {
            private const string CountString = "Count";
            private const string IndexerName = "Item[]";
            private const string KeysName = "Keys";
            private const string ValuesName = "Values";

            private IDictionary<TKey, TValue> _Dictionary;
            protected IDictionary<TKey, TValue> Dictionary
            {
                get { return _Dictionary; }
            }

            #region Constructors
            public ObservableDictionary()
            {
                _Dictionary = new Dictionary<TKey, TValue>();
            }
            public ObservableDictionary(IDictionary<TKey, TValue> dictionary)
            {
                _Dictionary = new Dictionary<TKey, TValue>(dictionary);
            }
            public ObservableDictionary(IEqualityComparer<TKey> comparer)
            {
                _Dictionary = new Dictionary<TKey, TValue>(comparer);
            }
            public ObservableDictionary(int capacity)
            {
                _Dictionary = new Dictionary<TKey, TValue>(capacity);
            }
            public ObservableDictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> comparer)
            {
                _Dictionary = new Dictionary<TKey, TValue>(dictionary, comparer);
            }
            public ObservableDictionary(int capacity, IEqualityComparer<TKey> comparer)
            {
                _Dictionary = new Dictionary<TKey, TValue>(capacity, comparer);
            }
            #endregion

            #region IDictionary<TKey,TValue> Members

            public void Add(TKey key, TValue value)
            {
                Insert(key, value, true);
            }

            public bool ContainsKey(TKey key)
            {
                return Dictionary.ContainsKey(key);
            }

            public ICollection<TKey> Keys
            {
                get { return Dictionary.Keys; }
            }

            public bool Remove(TKey key)
            {
                if (key == null) throw new ArgumentNullException("key");

                TValue value;
                Dictionary.TryGetValue(key, out value);
                var removed = Dictionary.Remove(key);
                if (removed)
                    //OnCollectionChanged(NotifyCollectionChangedAction.Remove, new KeyValuePair<TKey, TValue>(key, value));
                    OnCollectionChanged();

                return removed;
            }


            public bool TryGetValue(TKey key, out TValue value)
            {
                return Dictionary.TryGetValue(key, out value);
            }


            public ICollection<TValue> Values
            {
                get { return Dictionary.Values; }
            }


            public TValue this[TKey key]
            {
                get
                {
                    return Dictionary[key];
                }
                set
                {
                    Insert(key, value, false);
                }
            }


            #endregion


            #region ICollection<KeyValuePair<TKey,TValue>> Members


            public void Add(KeyValuePair<TKey, TValue> item)
            {
                Insert(item.Key, item.Value, true);
            }


            public void Clear()
            {
                if (Dictionary.Count > 0)
                {
                    Dictionary.Clear();
                    OnCollectionChanged();
                }
            }


            public bool Contains(KeyValuePair<TKey, TValue> item)
            {
                return Dictionary.Contains(item);
            }


            public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
            {
                Dictionary.CopyTo(array, arrayIndex);
            }


            public int Count
            {
                get { return Dictionary.Count; }
            }


            public bool IsReadOnly
            {
                get { return Dictionary.IsReadOnly; }
            }


            public bool Remove(KeyValuePair<TKey, TValue> item)
            {
                return Remove(item.Key);
            }


            #endregion


            #region IEnumerable<KeyValuePair<TKey,TValue>> Members


            public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
            {
                return Dictionary.GetEnumerator();
            }


            #endregion


            #region IEnumerable Members


            IEnumerator IEnumerable.GetEnumerator()
            {
                return ((IEnumerable)Dictionary).GetEnumerator();
            }


            #endregion


            #region INotifyCollectionChanged Members


            public event NotifyCollectionChangedEventHandler CollectionChanged;


            #endregion


            #region INotifyPropertyChanged Members


            public event PropertyChangedEventHandler PropertyChanged;


            #endregion


            public void AddRange(IDictionary<TKey, TValue> items)
            {
                if (items == null) throw new ArgumentNullException("items");


                if (items.Count > 0)
                {
                    if (Dictionary.Count > 0)
                    {
                        if (items.Keys.Any((k) => Dictionary.ContainsKey(k)))
                            throw new ArgumentException("An item with the same key has already been added.");
                        else
                            foreach (var item in items) Dictionary.Add(item);
                    }
                    else
                        _Dictionary = new Dictionary<TKey, TValue>(items);


                    OnCollectionChanged(NotifyCollectionChangedAction.Add, items.ToArray());
                }
            }


            private void Insert(TKey key, TValue value, bool add)
            {
                if (key == null) throw new ArgumentNullException("key");


                TValue item;
                if (Dictionary.TryGetValue(key, out item))
                {
                    if (add) throw new ArgumentException("An item with the same key has already been added.");
                    if (Equals(item, value)) return;
                    Dictionary[key] = value;


                    OnCollectionChanged(NotifyCollectionChangedAction.Replace, new KeyValuePair<TKey, TValue>(key, value), new KeyValuePair<TKey, TValue>(key, item));
                }
                else
                {
                    Dictionary[key] = value;

                    OnCollectionChanged(NotifyCollectionChangedAction.Add, new KeyValuePair<TKey, TValue>(key, value));
                }
            }


            private void OnPropertyChanged()
            {
                OnPropertyChanged(CountString);
                OnPropertyChanged(IndexerName);
                OnPropertyChanged(KeysName);
                OnPropertyChanged(ValuesName);
            }


            protected virtual void OnPropertyChanged(string propertyName)
            {
                if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }


            private void OnCollectionChanged()
            {
                OnPropertyChanged();
                if (CollectionChanged != null) CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
            }


            private void OnCollectionChanged(NotifyCollectionChangedAction action, KeyValuePair<TKey, TValue> changedItem)
            {
                OnPropertyChanged();
                if (CollectionChanged != null) CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, changedItem));
            }


            private void OnCollectionChanged(NotifyCollectionChangedAction action, KeyValuePair<TKey, TValue> newItem, KeyValuePair<TKey, TValue> oldItem)
            {
                OnPropertyChanged();
                if (CollectionChanged != null) CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, newItem, oldItem));
            }


            private void OnCollectionChanged(NotifyCollectionChangedAction action, IList newItems)
            {
                OnPropertyChanged();
                if (CollectionChanged != null) CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, newItems));
            }
        }
    }

Solution

  • When you enumerate a Dictionary<string, string>, you get an enumeration of KeyValuePair<String, String>. That's right there in your ObservableDictionary class:

    public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
    {
        return Dictionary.GetEnumerator();
    }
    

    So you're binding DataGrid.ItemsSource to Categories:

        ItemsSource="{Binding Categories}" 
    

    The DataGrid says "here's an IEnumerable of some random thing. Let's enumerate it and use one item for each row".

    Therefore, your grid row items are KeyValuePair<String, String>. That has two readonly properties: Key and Value. You're binding to Keys in your column: Binding="{Binding Keys}". No such property. When a binding fails, or appears to fail, check out the Output pane in Visual Studio at runtime.

    Here's the error you were getting in the Output pane:

    System.Windows.Data Error: 40 : BindingExpression path error: 'Keys' property not found on 'object' ''KeyValuePair`2' (HashCode=-1894483822)'. BindingExpression:Path=Keys; DataItem='KeyValuePair`2' (HashCode=-1894483822); target element is 'TextBlock' (Name=''); target property is 'Text' (type 'String')

    It's telling you that it's trying to find Keys on KeyValuePair, with no success. It's simplicity itself for you to go look at KeyValuePair, find out what it does have, and bind to that instead.

    Finally: If the user double clicks on a cell, the DataGrid will try to edit the cell. But Key and Value are read only, so it'll throw an exception. I'm making your grid readonly to prevent that from happening. I also cleaned up the element style, to save on copying and pasting with multiple columns.

    <DataGrid 
        Name="testGrid" 
        ItemsSource="{Binding Categories}" 
        Margin="21,22,370,50" 
        AutoGenerateColumns="False" 
        IsReadOnly="True"
        >
        <DataGrid.Resources>
            <Style TargetType="TextBlock" x:Key="TextElementStyle">
                <Setter Property="TextWrapping" Value="Wrap"/>
            </Style>
        </DataGrid.Resources>
        <DataGrid.Columns>
            <DataGridTextColumn 
                Header="Name" 
                Width="220" 
                Binding="{Binding Key}" 
                ElementStyle="{StaticResource TextElementStyle}" 
                />
            <DataGridTextColumn 
                Header="Value" 
                Width="220" 
                Binding="{Binding Value}" 
                ElementStyle="{StaticResource TextElementStyle}" 
                />
        </DataGrid.Columns>
    </DataGrid>
    

    Note that if you replace _categories with a new instance of ObservableDictionary, the UI will never know about it. It'll keep the old dictionary, because you are not raising any change notification when the value of Categories changes. That's fine if you don't need to, but be aware of it. A better way to code that would be this, which prevents that mistake from being made:

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
    
            DataContext = this;
    
            Categories.Add("Island", "IS");
            Categories.Add("Land", "LD");
            Categories.Add("Sea", "SE");
        }
    
        public ObservableDictionary<string, string> Categories { get; }
            = new ObservableDictionary<string, string>();
    }
    

    You're going to run into problems using margins for layout. It's much better to use Grid columns, StackPanels, etc. and use margins only for defining relative spacing between elements in a Grid or StackPanel. It becomes relatively simple to create dynamically resizable UIs, and to adjust the layout: If you want to make something wider, just set a Width on it and the rest of the layout will adjust, because it's relative instead of fixed.