2

我们有一个客户端-服务器应用程序,它需要动态构建视图。服务器将 XAML 字符串与数据 (Dctionary<string, string>) 一起发送到客户端,然后客户端将从收到的 Xaml 字符串构建视图并将数据绑定到视图。

这是示例 XAML 字符串:

     <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>

数据看起来像:

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

客户端将使用 XamlReader.Load() 构建视图,并创建一个窗口以将其作为内容托管。客户端还将接收到的数据分配给 Window.DataContext

 window.DataContext = dictionaryData;

由于两个 TextBox 从 Window 继承 DataContext,因此 Text 属性绑定到 Dictionary。绑定转换器“fieldBindingConverter”通过使用具有键的 ConverterParameter 从字典中获取正确的值。

因此,当第一次构建视图时,两个 TextBox 将相应地显示“1”和“John”。

当新数据到达客户端时出现问题

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

通过重置宿主窗口的 DataContext 不会使 TextBox 上的绑定刷新自身

window.DataContext = newDictionaryData;

实际上 TextBox 的 DataContext 仍然会缓存旧的数据值。

似乎 TextBox 仅在首次初始化时获取其父 DataContext 的副本,然后仅与该本地副本一起使用。

在这种情况下,拥有一个 ViewModel 并实现 INotifyPropertyChanged 似乎并不容易,因为键“ID_XX”可以在不同的视图之间变化,并且很难为这种动态性质定义一个模型类(我可能是错的) .

如果每次有新数据到达时都创建一个新的托管窗口(并设置了 DataContext),它确实可以正常工作,因为所有 TextBox 的 DataContext 都将为新的托管窗口派生新数据。

有谁知道如何让 TextBox “刷新”其 DataContext 以在父窗口上设置新的一组并“刷新”绑定?

4

3 回答 3

2

在 WPF 中,我们通常不会像这样将 a 设置DataContextWindow一个数据类型对象......但是这可能的。相反,我们通常创建一个特定的类,其中包含需要显示的所有属性,并且正如您所提到的,实现INotifyPropertyChanged接口。在您的情况下,我们将拥有一个Staff可以在 UI 中绑定到的类型属性:

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

然后在 XAML 中:

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

在这个小例子中,这不是绝对必要的,但在较大的项目中,可能还会显示其他数据。如果您将 设置Window.DataContext为您的Staff数据类型的一个实例,那么您会发现显示其他数据(例如Staff对象集合)会很棘手。同样,最好更新Staff属性,它会通知界面更新 UI,而不是DataContext不会更新 UI,因为它没有“连接”到界面。

属性更改的通知是通过INotifyPropertyChanged接口更新或“刷新”,当进行属性更改时,UI 控件中的值。所以你的答案是实现这个接口并确保你INotifyPropertyChanged.PropertyChangedEventHandler在属性值发生变化时调用。

更新>>>

哇!你真的不听,是吗?在 WPF 中,我们不会“刷新” DataContextINotifyPropertyChanged界面会在属性更改后“刷新” UI。这是解决此问题的一种可能方法:

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; }

...

然后你可以这样填写CustomObject

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>

现在你可以争辩说你不能这样做,或者你不想这样做,但归根结底,你必须做这样的事情来解决你的问题。

于 2013-10-09T09:45:05.513 回答
1

您可以考虑创建一个继承自 ObservalbeCollection 类的自定义 ObservableDictionary < T , U > 类。我已经完成了这项工作,尽管要解决这些问题需要做一些工作,但它已成为我最珍贵的定制课程之一。一些简短的代码作为建议:

/// <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);
        }
    }

}

那是基础课。类中的示例方法:

    /// <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;
    }

并且不要忘记枚举器:

        /// <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();
    }

现在显然我遗漏了一些实现,因为这最初源于我尝试(大部分成功)制作只读可观察字典。但基础确实有效。希望这有点用。

于 2013-10-09T11:01:56.423 回答
0

我通过将 TextBox 绑定到托管 Window 的 DataContext 的父级解决了这个问题。

感谢大家的评论。

于 2013-10-10T07:45:12.077 回答