รีเฟรช DataContext สำหรับการควบคุมลูกใน WPF

เรามีแอปพลิเคชันไคลเอ็นต์-เซิร์ฟเวอร์ซึ่งมีข้อกำหนดในการสร้างมุมมองแบบไดนามิก เซิร์ฟเวอร์จะส่งสตริง XAML พร้อมกับข้อมูล (Dctionary‹ string, string>) ไปยังไคลเอนต์ ซึ่งจะสร้างมุมมองจากสตริง Xaml ที่ได้รับและผูกข้อมูลเข้ากับ View

นี่คือตัวอย่างสตริง 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;

เนื่องจากกล่องข้อความทั้งสองสืบทอด DataContext จากหน้าต่าง คุณสมบัติ Text จึงเชื่อมโยงกับพจนานุกรม ตัวแปลงการรวม "fieldBindingConverter" ดึงค่าที่ถูกต้องออกจากพจนานุกรมโดยใช้ ConverterParameter ซึ่งมีคีย์

ดังนั้นกล่องข้อความทั้งสองจะแยก "1" และ "John" ตามลำดับเมื่อสร้าง View ครั้งแรก

ปัญหาเกิดขึ้นเมื่อข้อมูลใหม่มาถึงฝั่งไคลเอ็นต์

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

โดยการรีเซ็ต DataContext ของหน้าต่างโฮสติ้งจะไม่ทำให้การเชื่อมโยงในกล่องข้อความรีเฟรชตัวเอง

window.DataContext = newDictionaryData;

ในความเป็นจริง DataContext ของกล่องข้อความยังคงแคชค่าข้อมูลเก่า

ดูเหมือนว่ากล่องข้อความจะใช้สำเนาของ DataContext หลักเมื่อเริ่มต้นครั้งแรกเท่านั้น จากนั้นจึงใช้งานได้กับสำเนาในเครื่องนั้นหลังจากนั้นเท่านั้น

ปรากฏว่าไม่ใช่เรื่องง่ายที่จะมี ViewModel และใช้ INotifyPropertyChanged ในสถานการณ์นี้ เนื่องจากคีย์ "ID_XX" อาจแตกต่างกันไปตาม Views ที่แตกต่างกัน และมันยากที่จะกำหนดคลาส Model สำหรับลักษณะไดนามิกนี้ (ฉันอาจผิด) .

มันทำงานได้อย่างถูกต้องหากมีการสร้างหน้าต่างโฮสติ้งใหม่ (และตั้งค่า DataContext) ทุกครั้งที่มีข้อมูลใหม่เข้ามา เนื่องจาก DataContext ของกล่องข้อความทั้งหมดจะมีข้อมูลใหม่ที่ได้มาสำหรับหน้าต่างโฮสติ้งใหม่

ไม่มีใครรู้วิธีรับกล่องข้อความ "รีเฟรช" DataContext เพื่อใช้ชุดใหม่บนหน้าต่างหลักและ "รีเฟรช" การเชื่อมโยงหรือไม่


person wd113    schedule 09.10.2013    source แหล่งที่มา
comment
คุณจะเปลี่ยน DataContext ของหน้าต่างของคุณอย่างไรและเมื่อไหร่?   -  person Nitin    schedule 09.10.2013
comment
มันถูกกระตุ้นโดยเหตุการณ์เมื่อมีข้อมูลใหม่มาถึง   -  person wd113    schedule 09.10.2013


คำตอบ (3)


ใน WPF โดยทั่วไปเราจะไม่ตั้งค่า DataContext ของ Window ให้เป็นออบเจ็กต์ประเภทข้อมูลเดียวเช่นนี้... อย่างไรก็ตาม มัน เป็นไปได้ ที่เป็นไปได้ โดยปกติแล้ว เราจะสร้างคลาสเฉพาะที่มีคุณสมบัติทั้งหมดที่จำเป็นในการแสดง และตามที่คุณพูดถึงคือ ใช้งานอินเทอร์เฟซ 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 เราไม่ 'รีเฟรช' DataContext ส่วนอินเทอร์เฟซ INotifyPropertyChanged 'รีเฟรช' 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>

ตอนนี้คุณสามารถ สามารถ โต้แย้งว่าคุณไม่สามารถทำเช่นนี้ได้ด้วยเหตุผลนี้ หรือคุณไม่ต้องการทำเช่นนั้นด้วยเหตุผลนั้น แต่ท้ายที่สุดแล้ว คุณต้องทำอะไรเช่นนี้ เพื่อแก้ปัญหาของคุณ

person Sheridan    schedule 09.10.2013
comment
ขอบคุณสำหรับการตอบกลับ. ฉันเข้าใจถึงประโยชน์ของ INotifyPropertyChanged มันเป็นเพียงในสถานการณ์เฉพาะนี้ที่เราไม่สามารถใช้งานได้ และสงสัยว่าการรีเซ็ต DataContext จะทำเคล็ดลับเดียวกันหรือไม่ - person wd113; 09.10.2013
comment
ขออภัย แต่จะไม่เป็นเช่นนั้น เหตุใดคุณจึงใช้อินเทอร์เฟซ INotifyPropertyChanged ไม่ได้ - person Sheridan; 09.10.2013
comment
ข้อมูลคือ Dictionary‹string, string› และแต่ละกล่องข้อความจะผูกกับค่าของหนึ่ง keyValuePair ในพจนานุกรม ข้อมูลถูกสร้างขึ้นแบบไดนามิกและไม่มีคลาสโมเดล ดังนั้นเราจึงใช้ INPC ไม่ได้ ฉันใช้คลาส Staff เพื่อสาธิตความแตกต่างในค่าของ DataContext เท่านั้น มันอาจทำให้เกิดความสับสน ขอโทษด้วยกับเรื่องนั้น. - person wd113; 09.10.2013
comment
อาจช่วยได้หากคุณให้ตัวอย่างที่เหมาะสมอีกสักหน่อย เพื่อให้เราสามารถจัดการกับข้อจำกัดเหล่านี้ หรือแม้แต่ดูว่ามีอยู่จริงหรือไม่ - person ouflak; 09.10.2013
comment
มีหนทาง เสมอ เมื่อใดก็ตามที่คุณดึงข้อมูล เพียงแค่วนซ้ำเพื่อสร้างคอลเลกชั่นของออบเจ็กต์ประเภทข้อมูล และเพิ่มคุณสมบัติคอลเลกชั่นประเภทนั้นให้กับคลาสโมเดลมุมมองที่ใช้อินเทอร์เฟซ INotifyPropertyChanged ซึ่งคุณควรผูกเข้ากับ DataContext - person Sheridan; 09.10.2013
comment
โปรดดูคำถามที่อัปเดตซึ่งมีคำอธิบายปัญหาที่เรามี - person wd113; 09.10.2013

คุณอาจพิจารณาสร้างคลาส ObservableDictionary ‹ T , U > แบบกำหนดเองที่สืบทอดมาจากคลาส ObservalbeCollection ฉันทำสิ่งนี้แล้ว และถึงแม้จะต้องแก้ไขข้อบกพร่องบ้าง แต่ก็กลายเป็นหนึ่งในคลาสแต่งแบบกำหนดเองที่มีค่าที่สุดของฉัน โค้ดสั้นๆ บางส่วนเป็นคำแนะนำ:

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

ตอนนี้เห็นได้ชัดว่าฉันได้ละทิ้งการใช้งานบางอย่างเนื่องจากเดิมได้มาจากความพยายามของฉัน (ส่วนใหญ่ประสบความสำเร็จ) ในการสร้างพจนานุกรมที่สังเกตได้แบบอ่านอย่างเดียว แต่พื้นฐานก็ใช้งานได้ หวังว่านี่จะเป็นประโยชน์บ้าง

person ouflak    schedule 09.10.2013

ฉันได้แก้ไขปัญหาแล้วด้วยการทำให้กล่องข้อความเชื่อมโยงกับพาเรนต์ที่โฮสต์ DataContext ของ Window

ขอบคุณสำหรับความคิดเห็นของทุกคน

person wd113    schedule 10.10.2013