C# – Refresh DataContext for child controls in WPF

cdatacontextinheritancewpfxaml

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?

Best Answer

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.