Question

We have a client-server application which has the requirement of building the View dynamically. The server will send the XAML string together with the data (Dctionary< string, string>) to the client, which will then build the View from received Xaml string and bind the data to the View.

Here is the sample XAML string:

     <StackPanel>
        <TextBox>
            <TextBox.Text>
                <Binding RelativeSource="{{RelativeSource Self}}" Path="DataContext"   
                         Converter="{{StaticResource fieldBindingConverter}}" ConverterParameter="ID_Id"
                         UpdateSourceTrigger="PropertyChanged">
                 </Binding>
            </TextBox.Text>
        </TextBox> 

        <TextBox>
            <TextBox.Text>
                <Binding RelativeSource="{{RelativeSource Self}}" Path="DataContext"   
                         Converter="{{StaticResource fieldBindingConverter}}" ConverterParameter="ID_Name"
                         UpdateSourceTrigger="PropertyChanged">
                 </Binding>
            </TextBox.Text>
        </TextBox>        
     </StackPanel>

The data would look like:

new Dictionary<string, string>
    {
        {"ID_Id", "1"},
        {"ID_Name", "John"}
    };

The client will build the View by using XamlReader.Load(), and create a Window to host it as the Content. The client also assigns the received data to Window.DataContext

 window.DataContext = dictionaryData;

As the two TextBox's inherit the DataContext from Window, the Text property binds to the Dictionary. The binding converter "fieldBindingConverter" fetches the correct value out of the dictionary by using ConverterParameter which has the key.

So the Two TextBox's will disply "1" and "John" correspondingly when the View is first built.

The problem arises when a new data arrives at client side

    new Dictionary<string, string>
    {
        {"ID_Id", "2"},
        {"ID_Name", "Peter"}
    };

By resetting the DataContext of the hosting Window will not make the binding on the TextBox refresh itself

window.DataContext = newDictionaryData;

In fact the DataContext of the TextBox still cache the old data value.

It seems that TextBox only takes a copy of its parent DataContext when it's first initialized, and then only works with that local copy afterwards.

It also appears that it not easy to have a ViewModel and implement INotifyPropertyChanged in this scenario, as the key "ID_XX" can vary among different Views and it;s hard to have a Model class defined for this dynamic nature (I could be wrong).

It does work correctly if a new hosting Window is created (and DataContext is set) every time a new data arrives, because the DataContext of all TextBox's will have the new data derived for the new hosting window.

Does anyone know how to get the TextBox "refresh" its DataContext to take the new one set on the parent Window and "refresh" the binding?

Was it helpful?

Solution 3

I've solved the problem by making the TextBox binding to the parent hosting Window's DataContext.

Thanks for everyone's comments.

OTHER TIPS

In WPF, we don't generally set the DataContext of a Window to one data type object like this... however it is possible. Instead, we normally create a specific class that contains all of the properties required to be displayed and as you alluded to, implements the INotifyPropertyChanged interface. In your case, we would have a property of type Staff that we could bind to in the UI:

public string Staff
{
    get { return staff; }
    set { staff = value; NotifyPropertyChanged("Staff"); }
}

Then in XAML:

<Window>
    <StackPanel>
        <TextBox Text="{Binding Staff.Id}"/>
        <TextBox Text="{Binding Staff.Name}"/>
    </StackPanel>
</Window>

In this small example, this is not strictly necessary, but in larger projects, it is likely that there would be other data to display as well. If you set the Window.DataContext to just one instance of your Staff data type, then you would find it tricky to display other data, such as a collection of Staff objects. Likewise, it is better to update the Staff property, which will inform the interface to update the UI, rather than the DataContext which will not update the UI as it is not 'connected' to the interface.

It is the notification of property changes through the INotifyPropertyChanged interface that updates, or 'refreshes as you call it, the values in the UI controls when property changes are made. So your answer is to implement this interface and make sure that you call the INotifyPropertyChanged.PropertyChangedEventHandler when property value changes are made.

UPDATE >>>

Wow! You really aren't listening, are you? In WPF, we don't 'refresh' the DataContext, the INotifyPropertyChanged interface 'refreshes' the UI once properties have been changed. This is one possible way that you could fix this problem:

public class CustomObject
{
    public int Id { get; set; }
    public string Name { get; set; }
    ...
}

...

Dictionary<string, string> data = new Dictionary<string, string>
{
    {"ID_Id", "1"},
    {"ID_Name", "John"}
    ...
};

...

// Implement the `INotifyPropertyChanged` interface on this property
public ObservableCollection<CustomObject> CustomObjects { get; set; }

...

You could then fill your CustomObject like this:

CustomObjects = new ObservableCollection<CustomObject>();
CustomObject customObject = new CustomObject();
foreach (KeyValuePair<string, string> entry in data)
{
    if (entry.Key == "ID_Id") // Assuming this always comes first
    {
        customObject = new CustomObject();
        customObject.Id = int.Parse(entry.Value);
    }
    ...
    else if (entry.Key == "ID_Name") // Assuming this always comes lasst
    {
        customObject.Name = entry.Value;
        customObjects.Add(customObject);
    }
}

...

<ListBox ItemsSource="{Binding CustomObjects}">
    <ListBox.ItemTemplate>
        <DataTemplate DataType="{x:Type DataTypes:CustomObject}">
            <StackPanel>
                <TextBox Text="{Binding Id}"/>
                ...
                <TextBox Text="{Binding Name}"/>
            </StackPanel>
        </DataTemplate DataType="{x:Type DataTypes:CustomObject}">
    </ListBox.ItemTemplate>
</Window>

Now you could argue that you can't do this for this reason, or you don't want to do that for that reason, but at the end of the day, you have to do something like this to solve your problem.

You might consider creating a custom ObservableDictionary < T , U > class inherited from the ObservalbeCollection class. I've done this, and though it's been some work to get the kinks out, it's become one of my most prized custom classes. Some brief bits of code as a suggestion:

/// <summary>Dictionary changed event handler</summary>
/// <param name="sender">The dictionary</param>
/// <param name="e">The event arguments</param>
public delegate void NotifyDictionaryChangedEventHandler(object sender, NotifyDictionaryChangedEventArgs e);

public class CollectionDictionary<TKey, TValue> : ObservableCollection<TValue>
{
    #region Fields
    private ConcurrentDictionary<TKey, TValue> collectionDictionary = 
        new ConcurrentDictionary<TKey, TValue>();
    #endregion

    /// <summary>
    /// Initializes a new instance of the CollectionDictionary class
    /// </summary>
    public CollectionDictionary()
    {
    }

    /// <summary>
    /// Initializes a new instance of the CollectionDictionary class
    /// </summary>
    /// <param name="collectionDictionary">A dictionary</param>
    public CollectionDictionary(Dictionary<TKey, TValue> collectionDictionary)
    {
        for (int i = 0; i < collectionDictionary.Count; i++)
        {
            this.Add(collectionDictionary.Keys.ToList()[i], collectionDictionary.Values.ToList()[i]);                
        }
    }

    /// <summary>
    /// Initializes a new instance of the CollectionDictionary class
    /// </summary>
    /// <param name="collectionDictionary">A concurrent dictionary</param>
    public CollectionDictionary(ConcurrentDictionary<TKey, TValue> collectionDictionary)
    {
        this.collectionDictionary = collectionDictionary;
    }

    #region Events
    /// <summary>The dictionary has changed</summary>
    public event NotifyDictionaryChangedEventHandler DictionaryChanged;
    #endregion

    #region Indexers
    /// <summary> Gets the value associated with the specified key. </summary>
    /// <param name="key"> The key of the value to get or set. </param>
    /// <returns> Returns the Value property of the System.Collections.Generic.KeyValuePair&lt;TKey,TValue&gt;
    /// at the specified index. </returns>
    public TValue this[TKey key] 
    {
        get 
        {
            TValue tValue;

            if (this.collectionDictionary.TryGetValue(key, out tValue) && (key != null))
            {
                return this.collectionDictionary[key];
            }
            else
            {
                return tValue;
            }
        }

        ////set
        ////{
        ////    this.collectionDictionary[key] = value;

        ////    string tKey = key.ToString();
        ////    string tValue = this.collectionDictionary[key].ToString();
        ////    KeyValuePair<TKey, TValue> genericKeyPair = new KeyValuePair<TKey, TValue>(key, value);
        ////    List<KeyValuePair<TKey, TValue>> keyList = this.collectionDictionary.ToList();

        ////    for (int i = 0; i < keyList.Count; i++)
        ////    {
        ////        if (genericKeyPair.Key.ToString() == keyList[i].Key.ToString())
        ////        {
        ////            RemoveAt(i, String.Empty);
        ////            Insert(i, value.ToString(), String.Empty);
        ////        }
        ////    }
        ////} 
    }

    /// <summary>
    /// Gets the value associated with the specific index
    /// </summary>
    /// <param name="index">The index</param>
    /// <returns>The value at that index</returns>
    public new TValue this[int index]
    {
        get
        {
            if (index > (this.Count - 1))
            {
                return default(TValue);
            }
            else
            {
                return this.collectionDictionary.ToList()[index].Value;
            }
        }
    }
    #endregion

    /// <summary>
    /// Dictionary has changed. Notify any listeners.
    /// </summary>
    /// <param name="e">Evevnt arguments</param>
    protected virtual void OnDictionaryChanged(NotifyDictionaryChangedEventArgs e)
    {
        if (this.DictionaryChanged != null)
        {
            this.DictionaryChanged(this, e);
        }
    }

}

That's the basic class. An example method in the class:

    /// <summary> Adds a key/value pair to the 
    /// System.Collections.Concurrent.ConcurrentDictionary&lt;TKey,TValue&gt;
    /// if the key does not already exist, or updates a key/value pair in the 
    /// System.Collections.Concurrent.ConcurrentDictionary&lt;TKey,TValue&gt;
    /// if the key already exists. </summary>
    /// <param name="key"> The key to be added or whose value should be updated </param>
    /// <param name="addValueFactory">The function used to generate a value for an absent key</param>
    /// <param name="updateValueFactory">The function used to generate a new value for an 
    /// existing key based on the key's existing value</param>
    /// <returns> The new value for the key. This will be either be the result of addValueFactory
    /// (if the key was absent) or the result of updateValueFactory (if the key was
    /// present). </returns>
    public TValue AddOrUpdate(TKey key, Func<TKey, TValue> addValueFactory, Func<TKey, TValue, TValue> updateValueFactory)
    {
        TValue value;
        value = this.collectionDictionary.AddOrUpdate(key, addValueFactory, updateValueFactory);

        if (this.collectionDictionary.TryGetValue(key, out value))
        {
            ArrayList valueList = new ArrayList() { value };
            ArrayList keyList = new ArrayList() { key };
            NotifyDictionaryChangedEventArgs e = new NotifyDictionaryChangedEventArgs(
                NotifyCollectionChangedAction.Add,
                valueList,
                keyList);

            this.Add(value, string.Empty);
            this.OnDictionaryChanged(e);
        }

        return value;
    }

And don't forget the enumerators:

        /// <summary> Returns an enumerator that iterates through the 
    /// ObservableExtendedCollection&lt;TValue&gt;. </summary>
    /// <returns> An enumerator for the 
    /// underlying ObservableExtendedCollection&lt;TKey,TValue&gt;. </returns>   
    public new IEnumerator<TValue> GetEnumerator()
    {
        return (IEnumerator<TValue>)base.GetEnumerator();
    }

    /// <summary> Returns an enumerator that iterates through the 
    /// System.Collections.Concurrent.ConcurrentDictionary&lt;TKey,TValue&gt;. </summary>
    /// <returns> An enumerator for the 
    /// System.Collections.Concurrent.ConcurrentDictionary&lt;TKey,TValue&gt;. </returns>
    /// <param name="collectionFlag">Flag indicates to return the collection enumerator</param>
    public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator(bool collectionFlag = true)
    {
        return this.collectionDictionary.GetEnumerator();
    }

Now obviously I've left out some implementation as this is originally derived from my attempt (mostly successful) to make a readonly observable dictionary. But the basics do work. Hope this is of some use.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top