Refresh DataContext untuk kontrol anak di WPF

Kami memiliki aplikasi klien-server yang memiliki persyaratan untuk membangun Tampilan secara dinamis. Server akan mengirimkan string XAML bersama dengan data (string Dctionary‹, string>) ke klien, yang kemudian akan membangun Tampilan dari string Xaml yang diterima dan mengikat data ke Tampilan.

Berikut ini contoh string 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>

Datanya akan terlihat seperti:

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

Klien akan membangun Tampilan dengan menggunakan XamlReader.Load(), dan membuat Jendela untuk menampungnya sebagai Konten. Klien juga menugaskan data yang diterima ke Window.DataContext

 window.DataContext = dictionaryData;

Karena kedua TextBox mewarisi DataContext dari Window, properti Text mengikat Kamus. Konverter pengikatan "fieldBindingConverter" mengambil nilai yang benar dari kamus dengan menggunakan ConverterParameter yang memiliki kuncinya.

Jadi Dua Kotak Teks akan menampilkan "1" dan "John" secara bersamaan ketika Tampilan pertama kali dibuat.

Masalah muncul ketika data baru tiba di sisi klien

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

Dengan mereset DataContext dari Window hosting tidak akan membuat pengikatan pada TextBox menyegarkan dirinya sendiri

window.DataContext = newDictionaryData;

Faktanya DataContext dari TextBox masih menyimpan nilai data lama.

Tampaknya TextBox hanya mengambil salinan DataContext induknya saat pertama kali diinisialisasi, dan kemudian hanya berfungsi dengan salinan lokal tersebut setelahnya.

Tampaknya juga tidak mudah untuk memiliki ViewModel dan mengimplementasikan INotifyPropertyChanged dalam skenario ini, karena kunci "ID_XX" dapat bervariasi di antara Tampilan yang berbeda dan sulit untuk menentukan kelas Model untuk sifat dinamis ini (saya bisa saja salah) .

Ini berfungsi dengan benar jika Jendela hosting baru dibuat (dan DataContext diatur) setiap kali data baru tiba, karena DataContext dari semua TextBox akan memiliki data baru yang diperoleh untuk jendela hosting baru.

Adakah yang tahu cara membuat TextBox "menyegarkan" DataContext-nya untuk mengambil set baru di Jendela induk dan "menyegarkan" pengikatannya?


person wd113    schedule 09.10.2013    source sumber
comment
bagaimana dan kapan Anda mengubah DataContext jendela Anda?   -  person Nitin    schedule 09.10.2013
comment
itu dipicu oleh peristiwa saat data baru tiba.   -  person wd113    schedule 09.10.2013


Jawaban (3)


Di WPF, kami biasanya tidak menyetel DataContext dari Window ke satu objek tipe data seperti ini... namun adalah mungkin. Sebaliknya, kami biasanya membuat kelas tertentu yang berisi semua properti yang diperlukan untuk ditampilkan dan seperti yang Anda singgung, mengimplementasikan antarmuka INotifyPropertyChanged. Dalam kasus Anda, kami akan memiliki properti bertipe Staff yang dapat kami ikat di UI:

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

Kemudian di XAML:

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

Dalam contoh kecil ini, hal ini tidak sepenuhnya diperlukan, namun dalam proyek yang lebih besar, kemungkinan besar akan ada data lain untuk ditampilkan juga. Jika Anda menyetel Window.DataContext hanya ke satu contoh tipe data Staff, maka Anda akan kesulitan menampilkan data lain, seperti kumpulan objek Staff. Demikian pula, lebih baik memperbarui properti Staff, yang akan menginformasikan antarmuka untuk memperbarui UI, daripada DataContext yang tidak akan memperbarui UI karena tidak 'terhubung' ke antarmuka.

Ini adalah pemberitahuan perubahan properti melalui antarmuka INotifyPropertyChanged yang memperbarui, atau 'menyegarkan saat Anda menyebutnya, nilai di UI mengontrol saat perubahan properti dilakukan. Jadi jawaban Anda adalah mengimplementasikan antarmuka ini dan memastikan Anda memanggil INotifyPropertyChanged.PropertyChangedEventHandler ketika nilai properti diubah.

PEMBARUAN >>>

Wow! Anda benar-benar tidak mendengarkan, bukan? Di WPF, kami tidak 'menyegarkan' DataContext, antarmuka INotifyPropertyChanged 'menyegarkan' UI setelah properti diubah. Ini adalah salah satu cara yang mungkin untuk memperbaiki masalah ini:

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

...

Anda kemudian dapat mengisi CustomObject Anda seperti ini:

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>

Sekarang Anda bisa berpendapat bahwa Anda tidak dapat melakukan ini karena alasan ini, atau Anda tidak ingin melakukan itu karena alasan itu, namun pada akhirnya, Anda harus melakukan sesuatu seperti ini untuk memecahkan masalah Anda.

person Sheridan    schedule 09.10.2013
comment
Terima kasih balasannya. Saya memahami manfaat INotifyPropertyChanged. Hanya dalam skenario khusus inilah kami tidak dapat menggunakannya, dan bertanya-tanya apakah menyetel ulang DataContext akan melakukan trik yang sama. - person wd113; 09.10.2013
comment
Maaf, tapi tidak, itu tidak akan terjadi. Mengapa Anda tidak dapat menggunakan antarmuka INotifyPropertyChanged? - person Sheridan; 09.10.2013
comment
Datanya adalah kamus‹string, string›, dan setiap TextBox mengikat nilai satu keyValuePair dalam kamus. Data dihasilkan secara dinamis dan tidak ada kelas Model. Jadi kita tidak bisa menggunakan INPC. Saya menggunakan kelas Staf hanya untuk menunjukkan perbedaan nilai DataContext. Hal ini dapat menyebabkan kebingungan. Maaf tentang itu. - person wd113; 09.10.2013
comment
Mungkin akan membantu jika Anda dapat memberikan contoh yang lebih tepat, sehingga kita dapat menangani pembatasan ini, atau bahkan melihat apakah pembatasan tersebut benar-benar ada. - person ouflak; 09.10.2013
comment
selalu ada jalan. Kapan pun dan di mana pun Anda mengambil data, cukup ulangi dengan mengisi kumpulan objek tipe data dan tambahkan properti kumpulan tipe tersebut ke kelas model tampilan yang mengimplementasikan antarmuka INotifyPropertyChanged, yang kemudian harus Anda ikat ke DataContext. - person Sheridan; 09.10.2013
comment
Silakan lihat pertanyaan terbaru yang memiliki penjelasan tentang masalah yang kami miliki. - person wd113; 09.10.2013

Anda mungkin mempertimbangkan untuk membuat kelas ObservableDictionary ‹ T , U > khusus yang diwarisi dari kelas ObservalbeCollection. Saya telah melakukan ini, dan meskipun perlu kerja keras untuk menghilangkan masalah tersebut, ini menjadi salah satu kelas khusus saya yang paling berharga. Beberapa potongan kode singkat sebagai saran:

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

}

Itu kelas dasar. Contoh metode di kelas:

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

Dan jangan lupa enumeratornya:

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

Sekarang jelas saya telah meninggalkan beberapa implementasi karena ini awalnya berasal dari upaya saya (sebagian besar berhasil) untuk membuat kamus yang hanya dapat diamati dan dibaca. Tapi dasar-dasarnya berhasil. Semoga ini ada gunanya.

person ouflak    schedule 09.10.2013

Saya telah memecahkan masalah dengan membuat TextBox mengikat ke DataContext Window hosting induk.

Terima kasih atas komentar semua orang.

person wd113    schedule 10.10.2013