2011-03-09 19 views
20

ठीक है, यह मुझे थोड़ी देर के लिए परेशान कर रहा है। और मुझे आश्चर्य है कि कैसे दूसरों को निम्नलिखित मामला संभाल:कॉम्बोबॉक्स आइटमसोर्स बदल गया => चयनित इटिम बर्बाद हो गया है

<ComboBox ItemsSource="{Binding MyItems}" SelectedItem="{Binding SelectedItem}"/> 

DataContext ऑब्जेक्ट के कोड:

public ObservableCollection<MyItem> MyItems { get; set; } 
public MyItem SelectedItem { get; set; } 

public void RefreshMyItems() 
{ 
    MyItems.Clear(); 
    foreach(var myItem in LoadItems()) MyItems.Add(myItem); 
} 

public class MyItem 
{ 
    public int Id { get; set; } 
    public override bool Equals(object obj) 
    { 
     return this.Id == ((MyItem)obj).Id; 
    } 
} 

जाहिर है जब RefreshMyItems() विधि कॉम्बो बॉक्स कहा जाता है संग्रह बदली गई घटनाओं प्राप्त करता है, अपने आइटम अद्यतन करता है और करता है ताज़ा संग्रह में चयनित इटैम नहीं ढूंढें => चयनित इटैम को null पर सेट करता है। लेकिन नए संग्रह में सही आइटम का चयन करने के लिए मुझे Equals विधि का उपयोग करने के लिए कॉम्बो बॉक्स की आवश्यकता होगी।

दूसरे शब्दों में - आइटम्ससोर्स संग्रह में अभी भी सही MyItem है, लेकिन यह new ऑब्जेक्ट है। और मैं कॉम्बो बॉक्स को Equals जैसे कुछ स्वचालित रूप से चुनने के लिए उपयोग करना चाहता हूं (यह भी कठिन होता है क्योंकि पहले स्रोत संग्रह Clear() पर कॉल करता है जो संग्रह को रीसेट करता है और पहले से ही चयनित इटैम null पर सेट होता है)।

अद्यतन 2 नीचे दिए गए कोड को कॉपी करने से पहले कृपया ध्यान दें कि यह पूर्णता से बहुत दूर है! और ध्यान दें कि यह डिफ़ॉल्ट रूप से दो तरीकों से बंधे नहीं है।

public static class CBSelectedItem 
{ 
    public static object GetSelectedItem(DependencyObject obj) 
    { 
     return (object)obj.GetValue(SelectedItemProperty); 
    } 

    public static void SetSelectedItem(DependencyObject obj, object value) 
    { 
     obj.SetValue(SelectedItemProperty, value); 
    } 

    // Using a DependencyProperty as the backing store for SelectedIte. This enables animation, styling, binding, etc... 
    public static readonly DependencyProperty SelectedItemProperty = 
     DependencyProperty.RegisterAttached("SelectedItem", typeof(object), typeof(CBSelectedItem), new UIPropertyMetadata(null, SelectedItemChanged)); 


    private static List<WeakReference> ComboBoxes = new List<WeakReference>(); 
    private static void SelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 
    { 
     ComboBox cb = (ComboBox) d; 

     // Set the selected item of the ComboBox since the value changed 
     if (cb.SelectedItem != e.NewValue) cb.SelectedItem = e.NewValue; 

     // If we already handled this ComboBox - return 
     if(ComboBoxes.SingleOrDefault(o => o.Target == cb) != null) return; 

     // Check if the ItemsSource supports notifications 
     if(cb.ItemsSource is INotifyCollectionChanged) 
     { 
      // Add ComboBox to the list of handled combo boxes so we do not handle it again in the future 
      ComboBoxes.Add(new WeakReference(cb)); 

      // When the ItemsSource collection changes we set the SelectedItem to correct value (using Equals) 
      ((INotifyCollectionChanged) cb.ItemsSource).CollectionChanged += 
       delegate(object sender, NotifyCollectionChangedEventArgs e2) 
        { 
         var collection = (IEnumerable<object>) sender; 
         cb.SelectedItem = collection.SingleOrDefault(o => o.Equals(GetSelectedItem(cb))); 
        }; 

      // If the user has selected some new value in the combo box - update the attached property too 
      cb.SelectionChanged += delegate(object sender, SelectionChangedEventArgs e3) 
             { 
              // We only want to handle cases that actually change the selection 
              if(e3.AddedItems.Count == 1) 
              { 
               SetSelectedItem((DependencyObject)sender, e3.AddedItems[0]); 
              } 
             }; 
     } 

    } 
} 
+1

iv'e इस मुद्दे में आए और निम्नलिखित तरीके से इसे हल http://stackoverflow.com/questions/12337442/proper-use-of-propertychangedtrigger-and-changepropertyaction/12341649#12341649 –

उत्तर

7

मानक ComboBox में यह तर्क नहीं है। और जैसा कि आपने SelectedItem का उल्लेख किया है null पहले से ही Clear पर कॉल करने के बाद, इसलिए ComboBox को आपके बारे में कोई जानकारी नहीं है कि आप बाद में उसी आइटम को जोड़ना चाहते हैं और इसलिए इसे चुनने के लिए कुछ भी नहीं है। ऐसा कहा जा रहा है कि आपको पहले चयनित आइटम को मैन्युअल रूप से याद रखना होगा और आपके द्वारा अपडेट किए जाने के बाद संग्रह को मैन्युअल रूप से चयन को पुनर्स्थापित करना होगा। कुछ इस तरह आमतौर पर यह किया जाता है:

public void RefreshMyItems() 
{ 
    var previouslySelectedItem = SelectedItem; 

    MyItems.Clear(); 
    foreach(var myItem in LoadItems()) MyItems.Add(myItem); 

    SelectedItem = MyItems.SingleOrDefault(i => i.Id == previouslySelectedItem.Id); 

} 

आप सभी ComboBoxes (या शायद सभी Selector नियंत्रण) के लिए एक ही व्यवहार को लागू करना चाहते हैं, तो आप बनाने पर विचार कर सकते हैं एक Behavior (एक attached property या blend behavior)। यह व्यवहार SelectionChanged और CollectionChanged ईवेंट की सदस्यता लेगा और उपयुक्त होने पर चयनित आइटम को सहेज/पुनर्स्थापित करेगा।

+0

हाँ, बिल्कुल मेरे विचार :) हालांकि मैं इस मामले के लिए एक संलग्न संपत्ति लिखूंगा। संकेत के लिए धन्यवाद! (मैंने संलग्न संपत्ति कोड के साथ पोस्ट अपडेट किया है) – Jefim

+0

यह कॉम्बोबॉक्स और डब्ल्यूपीएफ, सिल्वरलाइट और यूडब्लूपी में अन्य चयनकर्ता नियंत्रणों के साथ एक आम समस्या है। कोड के पीछे कोड लिखने के बिना सभी चयनकर्ता नियंत्रणों और प्लेटफार्मों में समस्या को ठीक करने का एक प्रस्तावित समाधान यहां दिया गया है। http://stackoverflow.com/questions/36003805/uwp-silverlight-combobox-selector-itemssource-selecteditem-race-condition-solu –

+0

@ मेलबर्न डेवलपर लिंक नीचे है - रिटर्न पृष्ठ नहीं मिला। – tomosius

0

आप अपने संग्रह से सही SlectedItem चयन करने के लिए एक valueconverter उपयोग करने पर विचार कर सकते हैं:

अद्यतन बस मामले में किसी को (के रूप में अपने जवाब में पाव्लो ग्लाज़्कोव द्वारा प्रस्तावित एक संलग्न संपत्ति) एक ही समस्या है

+0

यह एक है समाधान, ज़ाहिर है। हालांकि फिर एक सवाल आता है - क्या इसे कॉम्बो बक्से के पूरे सेट पर लागू करने का कोई तरीका है। मुझे यह भी नहीं लगता कि एक मूल्य कनवर्टर के भीतर बाध्यकारी लक्ष्य तक पहुंचने का एक अच्छा तरीका है (और मुझे सही विकल्प चुनने के लिए आइटम्ससोर्स तक पहुंच की आवश्यकता होगी)। – Jefim

+0

आप एक मल्टीवाइवल कनवर्टर का उपयोग कर सकते हैं और – biju

+0

बाध्यकारी के रूप में अपने संग्रह में पास कर सकते हैं वैल्यू कनवर्टर इससे मदद नहीं करेगा क्योंकि 'साफ़' कहने के बाद चयन करने के लिए कुछ भी नहीं है। आपको किसी भी तरह से पहले चयनित आइटम को स्टोर करने की आवश्यकता होगी। –

13

इस के लिए शीर्ष गूगल परिणाम है "WPF ItemsSource के बराबर है" अभी, प्रश्न में के रूप में ही दृष्टिकोण की कोशिश कर किसी को हां, तो यह जब तक आप पूरी तरह से समानता कार्यों को लागू काम करता है।

public class MyItem : IEquatable<MyItem> 
{ 
    public int Id { get; set; } 

    public bool Equals(MyItem other) 
    { 
     if (Object.ReferenceEquals(other, null)) return false; 
     if (Object.ReferenceEquals(other, this)) return true; 
     return this.Id == other.Id; 
    } 

    public sealed override bool Equals(object obj) 
    { 
     var otherMyItem = obj as MyItem; 
     if (Object.ReferenceEquals(otherMyItem, null)) return false; 
     return otherMyItem.Equals(this); 
    } 

    public override int GetHashCode() 
    { 
     return this.Id.GetHashCode(); 
    } 

    public static bool operator ==(MyItem myItem1, MyItem myItem2) 
    { 
     return Object.Equals(myItem1, myItem2); 
    } 

    public static bool operator !=(MyItem myItem1, MyItem myItem2) 
    { 
     return !(myItem1 == myItem2); 
    } 
} 

मैं सफलतापूर्वक एक बहु चयन ListBox, जहां listbox.SelectedItems.Add(item) मिलान आइटम का चयन करने में विफल रहा था के साथ इस परीक्षण किया, लेकिन उसके बाद मैं item पर ऊपर लागू किया काम: यहाँ एक पूरा MyItem कार्यान्वयन है।

+0

आपके उत्तर के लिए धन्यवाद! मैं ऑपरेटर == और ऑपरेटर लागू करने के लिए भूल गया था! = ... और यह मुझे सिरदर्द पैदा कर रहा था! – cplotts

-3
public MyItem SelectedItem { get; set; } 
    private MyItem selectedItem ; 
    // <summary> 
    /////// 
    // </summary> 
    public MyItem SelectedItem 
    { 
     get { return selectedItem ; } 
     set 
     { 
      if (value != null && selectedItem != value) 
      { 
       selectedItem = value; 
       if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs("SelectedItem ")); } 
      } 
     } 
    } 
+0

यह देखना बेहद मुश्किल है कि यह प्रश्न के उत्तर का उत्तर कैसे है। यहां कुछ स्पष्टीकरण के बिना, ऐसा लगता है जैसे आपने कुछ यादृच्छिक कोड पोस्ट किया है। –

6

दुर्भाग्य से एक चयनकर्ता वस्तु पर ItemsSource सेट करते समय इसे तुरंत SelectedValue या SelectedItem शून्य पर भले ही इसी मद नई ItemsSource में है निर्धारित करता है।

कोई फर्क नहीं पड़ता कि आप बराबर लागू करते हैं .. फ़ंक्शंस या आप अपने चयनित वैल्यू के लिए एक स्पष्ट रूप से तुलनीय प्रकार का उपयोग करते हैं।

ठीक है, आप आइटम्ससोर्स सेट करने और पुनर्स्थापित करने से पहले चयनित इटैम/वैल्यू को सहेज सकते हैं। लेकिन क्या होगा यदि चयनित इटैम/वैल्यू पर बाध्यकारी है जिसे दो बार कहा जाएगा: शून्य मूल पर पुनर्स्थापित करें।

यह अतिरिक्त ओवरहेड है और यहां तक ​​कि यह कुछ अवांछित व्यवहार का कारण बन सकता है।

यहां एक समाधान है जिसे मैंने बनाया है। किसी भी चयनकर्ता वस्तु के लिए काम करेगा। आइटम्ससोर्स सेट करने से पहले बस चयनित वैल्यू बाध्यकारी साफ़ करें।

यूपीडी: हैंडलरों में अपवादों से बचाने के लिए प्रयास/अंत में जोड़ा गया, बाध्यकारी के लिए भी शून्य जांच जोड़ा गया।

   <telerik:RadComboBox Grid.Column="1" x:Name="cmbDevCamera" DataContext="{Binding Settings}" SelectedValue="{Binding SelectedCaptureDevice}" 
            SelectedValuePath="guid" e:ComboBoxItemsSourceDecorator.ItemsSource="{Binding CaptureDeviceList}" > 
       </telerik:RadComboBox> 

यूनिट टेस्ट

यहाँ एक इकाई परीक्षण का मामला साबित है कि यह काम करता है:

public static class ComboBoxItemsSourceDecorator 
{ 
    public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.RegisterAttached(
     "ItemsSource", typeof(IEnumerable), typeof(ComboBoxItemsSourceDecorator), new PropertyMetadata(null, ItemsSourcePropertyChanged) 
    ); 

    public static void SetItemsSource(UIElement element, IEnumerable value) 
    { 
     element.SetValue(ItemsSourceProperty, value); 
    } 

    public static IEnumerable GetItemsSource(UIElement element) 
    { 
     return (IEnumerable)element.GetValue(ItemsSourceProperty); 
    } 

    static void ItemsSourcePropertyChanged(DependencyObject element, 
        DependencyPropertyChangedEventArgs e) 
    { 
     var target = element as Selector; 
     if (element == null) 
      return; 

     // Save original binding 
     var originalBinding = BindingOperations.GetBinding(target, Selector.SelectedValueProperty); 

     BindingOperations.ClearBinding(target, Selector.SelectedValueProperty); 
     try 
     { 
      target.ItemsSource = e.NewValue as IEnumerable; 
     } 
     finally 
     { 
      if (originalBinding != null) 
       BindingOperations.SetBinding(target, Selector.SelectedValueProperty, originalBinding); 
     } 
    } 
} 

यहाँ एक XAML उदाहरण है। मानक बाइंडिंग का उपयोग करते समय परीक्षण विफल होने के लिए बस #define USE_DECORATOR पर टिप्पणी करें।

#define USE_DECORATOR 

using System.Collections; 
using System.Collections.Concurrent; 
using System.Collections.Generic; 
using System.Security.Permissions; 
using System.Threading.Tasks; 
using System.Windows; 
using System.Windows.Controls; 
using System.Windows.Controls.Primitives; 
using System.Windows.Data; 
using System.Windows.Threading; 
using FluentAssertions; 
using ReactiveUI; 
using ReactiveUI.Ext; 
using ReactiveUI.Fody.Helpers; 
using Xunit; 

namespace Weingartner.Controls.Spec 
{ 
    public class ComboxBoxItemsSourceDecoratorSpec 
    { 
     [WpfFact] 
     public async Task ControlSpec() 
     { 
      var comboBox = new ComboBox(); 
      try 
      { 

       var numbers1 = new[] {new {Number = 10, i = 0}, new {Number = 20, i = 1}, new {Number = 30, i = 2}}; 
       var numbers2 = new[] {new {Number = 11, i = 3}, new {Number = 20, i = 4}, new {Number = 31, i = 5}}; 
       var numbers3 = new[] {new {Number = 12, i = 6}, new {Number = 20, i = 7}, new {Number = 32, i = 8}}; 

       comboBox.SelectedValuePath = "Number"; 
       comboBox.DisplayMemberPath = "Number"; 


       var binding = new Binding("Numbers"); 
       binding.Mode = BindingMode.OneWay; 
       binding.UpdateSourceTrigger=UpdateSourceTrigger.PropertyChanged; 
       binding.ValidatesOnDataErrors = true; 

#if USE_DECORATOR 
       BindingOperations.SetBinding(comboBox, ComboBoxItemsSourceDecorator.ItemsSourceProperty, binding); 
#else 
       BindingOperations.SetBinding(comboBox, ItemsControl.ItemsSourceProperty, binding); 
#endif 

       DoEvents(); 

       var selectedValueBinding = new Binding("SelectedValue"); 
       BindingOperations.SetBinding(comboBox, Selector.SelectedValueProperty, selectedValueBinding); 

       var viewModel = ViewModel.Create(numbers1, 20); 
       comboBox.DataContext = viewModel; 

       // Check the values after the data context is initially set 
       comboBox.SelectedIndex.Should().Be(1); 
       comboBox.SelectedItem.Should().BeSameAs(numbers1[1]); 
       viewModel.SelectedValue.Should().Be(20); 

       // Change the list of of numbers and check the values 
       viewModel.Numbers = numbers2; 
       DoEvents(); 

       comboBox.SelectedIndex.Should().Be(1); 
       comboBox.SelectedItem.Should().BeSameAs(numbers2[1]); 
       viewModel.SelectedValue.Should().Be(20); 

       // Set the list of numbers to null and verify that SelectedValue is preserved 
       viewModel.Numbers = null; 
       DoEvents(); 

       comboBox.SelectedIndex.Should().Be(-1); 
       comboBox.SelectedValue.Should().Be(20); // Notice that we have preserved the SelectedValue 
       viewModel.SelectedValue.Should().Be(20); 


       // Set the list of numbers again after being set to null and see that 
       // SelectedItem is now correctly mapped to what SelectedValue was. 
       viewModel.Numbers = numbers3; 
       DoEvents(); 

       comboBox.SelectedIndex.Should().Be(1); 
       comboBox.SelectedItem.Should().BeSameAs(numbers3[1]); 
       viewModel.SelectedValue.Should().Be(20); 


      } 
      finally 
      { 
       Dispatcher.CurrentDispatcher.InvokeShutdown(); 
      } 
     } 

     public class ViewModel<T> : ReactiveObject 
     { 
      [Reactive] public int SelectedValue { get; set;} 
      [Reactive] public IList<T> Numbers { get; set; } 

      public ViewModel(IList<T> numbers, int selectedValue) 
      { 
       Numbers = numbers; 
       SelectedValue = selectedValue; 
      } 
     } 

     public static class ViewModel 
     { 
      public static ViewModel<T> Create<T>(IList<T> numbers, int selectedValue)=>new ViewModel<T>(numbers, selectedValue); 
     } 

     /// <summary> 
     /// From http://stackoverflow.com/a/23823256/158285 
     /// </summary> 
     public static class ComboBoxItemsSourceDecorator 
     { 
      private static ConcurrentDictionary<DependencyObject, Binding> _Cache = new ConcurrentDictionary<DependencyObject, Binding>(); 

      public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.RegisterAttached(
       "ItemsSource", typeof(IEnumerable), typeof(ComboBoxItemsSourceDecorator), new PropertyMetadata(null, ItemsSourcePropertyChanged) 
      ); 

      public static void SetItemsSource(UIElement element, IEnumerable value) 
      { 
       element.SetValue(ItemsSourceProperty, value); 
      } 

      public static IEnumerable GetItemsSource(UIElement element) 
      { 
       return (IEnumerable)element.GetValue(ItemsSourceProperty); 
      } 

      static void ItemsSourcePropertyChanged(DependencyObject element, 
          DependencyPropertyChangedEventArgs e) 
      { 
       var target = element as Selector; 
       if (target == null) 
        return; 

       // Save original binding 
       var originalBinding = BindingOperations.GetBinding(target, Selector.SelectedValueProperty); 
       BindingOperations.ClearBinding(target, Selector.SelectedValueProperty); 
       try 
       { 
        target.ItemsSource = e.NewValue as IEnumerable; 
       } 
       finally 
       { 
        if (originalBinding != null) 
         BindingOperations.SetBinding(target, Selector.SelectedValueProperty, originalBinding); 
       } 
      } 
     } 

     [SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)] 
     public static void DoEvents() 
     { 
      DispatcherFrame frame = new DispatcherFrame(); 
      Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback(ExitFrame), frame); 
      Dispatcher.PushFrame(frame); 
     } 

     private static object ExitFrame(object frame) 
     { 
      ((DispatcherFrame)frame).Continue = false; 
      return null; 
     } 


    } 
} 
+2

यह समाधान डिफ़ॉल्ट कॉम्बो बॉक्स के लिए भी काम करता है। हालांकि, आपको यह जांचना होगा कि कोड की अंतिम पंक्ति निष्पादित करने से पहले मूल बाइंडिंग शून्य नहीं है, क्योंकि आइटम स्रोत के पहले लोड पर मूल बाध्यकारी शून्य होगी। – alexandrudicu

+0

यह 4.5.1 रनटाइम के तहत चलने के बाद से अचानक एक ही समस्या के लिए बहुत उपयोगी था। धन्यवाद! –

+0

इसे और अधिक अपरिवर्तित किया जाना चाहिए क्योंकि यह एकमात्र वैध स्वीकार्य उत्तर है क्योंकि यह पूरी तरह से समस्या को समाप्त करता है और बहु ​​थ्रेडेड ऐप में काम करता है। – bokibeg

-1

मेरे सिर के बाल के आधे खो और कई बार अपने कीबोर्ड स्मैश के बाद, मैं लगता है कि बता गया नियंत्रण के लिए, यह बेहतर SelectedItem लिखने के लिए नहीं, SelectedIndex और ItemsSource XAML में बाध्यकारी अभिव्यक्ति है के रूप में हम नहीं कर सकते निश्चित रूप से आइटम्ससोर्स संपत्ति का उपयोग करते समय आइटमसोर्स बदल गया है या नहीं।

खिड़की या उपयोगकर्ता नियंत्रण निर्माता में मैं कंबोबॉक्स की आइटम्ससोर्स प्रॉपर्टी सेट करता हूं, फिर विंडो या उपयोगकर्ता नियंत्रण के लोड किए गए ईवेंट हैंडलर में, मैं बाध्यकारी अभिव्यक्ति सेट करता हूं और यह पूरी तरह से काम करता है। यदि मैं XAML में "चयनित इटैम" के बिना आइटम्ससोर्स बाध्यकारी अभिव्यक्ति सेट करता हूं, तो मुझे किसी भी इवेंट हैंडलर को चयनित इटैम बाइंडिंग अभिव्यक्ति सेट करने के लिए नहीं मिलेगा, जबकि कंबोबॉक्स को शून्य संदर्भ (चयनित इंडेक्स = -1) के साथ स्रोत अपडेट करने से रोकता है।

0

इस समस्या का वास्तविक समाधान नई सूची में मौजूद आइटमों को नहीं निकालना है। अर्थात। पूरी सूची को साफ़ न करें, केवल उन लोगों को हटा दें जो नई सूची में नहीं हैं और फिर उन लोगों को जोड़ें जिन्हें नई सूची में पुरानी सूची में नहीं था।

उदाहरण।

वर्तमान कॉम्बो बॉक्स आइटम एप्पल, संतरा, केला

नए कॉम्बो बॉक्स आइटम एप्पल, संतरा, नाशपाती

नए आइटम को भरने के लिए केले कॉम्बो निकालें और जोड़े नाशपाती

अब धनुष अभी भी उन वस्तुओं के लिए मान्य है जिन्हें आप चुन सकते थे और यदि आइटम चुने गए थे तो आइटम अब साफ़ हो गए हैं।

0

मैं सिर्फ एक बहुत ही सरल ओवरराइड लागू किया है और यह लेकिन यह आंतरिक तर्क के झुंड को काट देता है, तो मुझे यकीन है कि यह सुरक्षित समाधान है नहीं कर रहा हूँ, नेत्रहीन काम कर रहा है:

public class MyComboBox : ComboBox 
{ 
    protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e) 
    { 
     return; 
    } 
} 

तो अगर आप इस का उपयोग नियंत्रण आइटम/आइटम स्रोत को बदलना चयनित वैल्यू और टेक्स्ट को प्रभावित नहीं करेगा - वे छूटे रहेंगे।

अगर आपको समस्याएं आती हैं तो कृपया मुझे बताएं।

संबंधित मुद्दे