簡體   English   中英

使用 WPF、MVVM 和 SelectionChanged 事件處理 ComboBox 選擇

[英]Handling a ComboBox Selection using WPF,MVVM, and the SelectionChanged Event

我有一個組合框。 當用戶嘗試更改 ComboBox 上的 selectedItem 時,我希望彈出一個 MessageBox 並確認他們是否要更改他們的選擇。 如果不是,則選擇不應更改。 這是我最初擁有的:

選擇站點視圖 (XAML):

        <ComboBox Name="SiteSelectionComboBox"
                  ItemsSource="{Binding SiteList}"
                  SelectedItem="{Binding SelectedSite}"/>

選擇站點視圖模型:

        private List<string> siteList;
        public List<string> SiteList
        {
            get { return siteList; }
            set { siteList = value;
                OnPropertyChanged();
            }
        }

        private string selectedSite;
        public string SelectedSite
        {
            get { return selectedSite; }
            set {
                if (CanChangeSite())
                {
                    selectedSite = value;
                    OnPropertyChanged();
                }
                //if false, I DONT want the combobox to change it's selection. 
                //What I end up getting is a mismatch. The SelectedSite never gets updated to the new 'value', but the ComboBox still changes
            }
        }

        private bool CanChangeSite()
        {
            MessageBoxResult result = MessageBox.Show("Are you sure?", "WARNING", MessageBoxButton.YesNo, MessageBoxImage.Question);
            if (result == MessageBoxResult.Yes)
                return true;
            else return false;
        }

我無法讓它工作。 我猜因為綁定是雙向的,即使我沒有更新我的 SelectedSite 值,ComboBox 仍然會從 UI 更改 SelectedItem 值,導致我的綁定可能不匹配的情況。

然后我繼續嘗試處理 SelectionChanged 事件,但沒有成功。 這是我嘗試實施它的方式:

選擇站點視圖 (XAML):

        xmlns:i="http://schemas.microsoft.com/xaml/behaviors"

        <ComboBox Name="SiteSelectionComboBox"
                  ItemsSource="{Binding SiteList}"
                  SelectedItem="{Binding SelectedSite}">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="SelectionChanged">
                    <i:InvokeCommandAction Command="{Binding SelectionChangedCommand}" PassEventArgsToCommand="True"/>
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </ComboBox>

選擇站點視圖模型:

        public ICommand SelectionChangedCommand { get; set; }
        public SelectSiteViewModel(MainViewModel main)
        {
            SelectionChangedCommand = new RelayCommand(o => SelectionChangedAction(o));
        }
        public void SelectionChangedAction(object param)
        {
            MessageBox.Show("selection changed command activated, param = " + param.ToString());
            if (param == null)
                MessageBox.Show("NULL");
            //MessageBox.Show("Added item: " + param.AddedItems[0]); 
            //DOESNT WORK
        }

我嘗試將我的 ComboBox 選擇事件參數作為參數傳遞給 ViewModel。 當我打印它的 ToString 值時,我得到:“System.Windows.Controls.SelectionChangedEventArgs”,在調試器上,我可以看到它具有我感興趣的“AddedItems”和“RemovedItems”屬性。但是,我不能不要訪問這些項目。 是因為對象沒有正確傳遞嗎? 我將如何訪問這些項目以便我可以拒絕組合框中的更改? 使用此事件處理程序是執行此操作的最佳方法嗎?

我看到了一個使用 MVVMLite 的解決方案,如果可能的話,我寧願不使用任何額外的依賴項。 提前致謝!

您不想在 MVVM 應用程序的視圖模型中處理 UI 事件。

這是一個經典的屬性驗證場景。
您必須通過實施INotifyDataErrorInfo來驗證該屬性。 然后您的視圖模型將忽略無效的屬性值,例如當INotifyDataErrorInfo.HasErrors返回 true 或當INotifyDataErrorInfo.GetErrors為特定屬性返回錯誤時,視圖模型知道它處於無效狀態。

發出無效屬性值的信號將自動(通過綁定引擎)觸發視圖向用戶顯示錯誤指示器。

在 GUI 中強制值不是推薦的解決方案,因為這對用戶來說就像黑魔法一樣。 從他的角度來看,他的選擇會神奇地變成其他東西,可能無法識別。

在這種情況下,最好指示無效選擇並讓用戶明確更改它。
甚至更好:通過禁用(灰色)或過濾無效選項來禁止無效輸入。 這樣用戶只能選擇有效的項目。 這是最簡潔的解決方案,可以避免顯示輸入錯誤,從而提供流暢流暢(不間斷)的用戶體驗。

我建議從 UI 中過濾無效的輸入選項,因為這是最優雅的解決方案。 它以多種方式改善了用戶體驗。 它不會中斷流程,也不會用無效或禁用的元素使 UI 混亂。

如果您想強制綁定目標拒絕無效值,您可以從控件(需要擴展控件或將相關邏輯作為附加行為實現)或從視圖模型執行此操作。

重要提示:由於您要求的是 MVVM 解決方案,因此以下所有示例(第一個和最后一個除外)都希望視圖模型實現INotifyDataErrorInfo (或一般的綁定驗證)。
這些示例也適用於視圖級綁定驗證( Binding.ValidationRules ,您應該在 MVVM 場景中盡量避免這種情況)。

過濾數據展示的無效項(推薦)

此解決方案通常不需要INotifyDataErrorInfo或綁定驗證。 它不允許任何錯誤。 因此,不需要錯誤反饋。

因為在 WPF 中,綁定引擎會自動綁定到任何集合的ICollectionView ,我們可以簡單地使用默認的ICollectionView來過濾無效的項目,以防止它們出現在 UI 中。 以這種方式過濾不會修改集合本身:

MainViewModel.cs

class MainViewModel : INotifyPropertyChanged
{
  public ObservableCollection<object> SourceCollection { get; set; }

  public MainViewModel()
  {
    this.SourceCollection = new ObservableCollection<object>();

    ICollectionView collectionView = CollectionViewSource.GetDefaultView(this.SourceCollection);
    collectionView.Filter = IsValueValid;
  }

  // Return true if the item should appear in the collection view
  private bool IsValueValid(object itemToValidate) => true;
}

在擴展控件中強制使用有效值

您將在依賴屬性更改回調中實現邏輯。
對於ComboBox (或任何Selector ),我們可以通過覆蓋處理程序來處理Selector.SelectionChanged事件:

自定義組合框.cs

public class CustomComboBox : ComboBox
{
  protected override void OnSelectionChanged(SelectionChangedEventArgs e)
  {
    base.OnSelectionChanged(e);

    BindingExpression selectedItemPropertyBinding = GetBindingExpression(Selector.SelectedItemProperty);
    var isSelectedItemInvalid = selectedItemPropertyBinding.HasValidationError;
    if (isSelectedItemInvalid)
    {
      this.SelectedItem = e.RemovedItems[0];
    }
  }
}

在代碼隱藏中強制使用有效值

該示例將在代碼隱藏中處理ComboBoxSelector.SelectionChanged事件:

主窗口.xaml.cs

private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
  var comboBox = sender as ComboBox;

  BindingExpression selectedItemPropertyBinding = comboBox.GetBindingExpression(Selector.SelectedItemProperty);
  if (selectedItemPropertyBinding.HasValidationError)
  {
    comboBox.SelectedItem = e.RemovedItems[0];
  }
}

在視圖模型中強制使用有效值(不推薦)

此解決方案通常不需要INotifyDataErrorInfo或綁定驗證。 它不允許任何錯誤。 因此,不需要錯誤反饋。

驗證綁定到Selector.SelectedItem屬性的源屬性后,如果驗證失敗,視圖模型類將覆蓋無效值。

除了設計決策(尤其是用戶體驗設計)之外,僅從所需的代碼中,您就可以看出這不是最優雅的方式。:

主視圖模型

private object selectedDataItem;
public object SelectedDataItem
{
  get => this.selectedDataItem;
  set
  {
    if (value == this.SelectedDataItem)
    {
      return;
    }
    
    var oldItem = this.selectedDataItem;
    this.selectedDataItem = value;
    OnPropertyChanged();

    if (IsValueValid(value))
    {
      // Defer overriding the property value because we must leave the scope
      // to allow the binding to complete the current transaction.
      // Then we can start a new to actually override the current value.
      // This example uses a background thread to allow the current context to leave the scope.
      // Note: you can use Dispatcher.InvokeAsync (do not await it) to achieve the same. But you probably want to avoid tight coupling to the Singleton.
      Task.Run(() => this.SelectedDataItem = oldItem);
    }
  }
}

或者,通過將Binding.IsAsync設置為trueBinding配置為異步執行。
這樣就不需要后台線程或Dispatcher.InvokeAsync來“欺騙” Binding流程:

<ComboBox SelectedItem="{Binding SelectedDataItem, IsAsync=True}" />
private object selectedDataItem;
public object SelectedDataItem
{
  get => this.selectedDataItem;
  set
  {
    if (value == this.SelectedDataItem)
    {
      return;
    }
    
    var oldItem = this.selectedDataItem;
    this.selectedDataItem = value;
    OnPropertyChanged();

    if (IsValueValid(value))
    {
      // Because the Binding.IsAsync is set to true, 
      // the Binding can continue to leave the scope while the property executes. 
      // This allows the second PropertyChanged to trigger the Binding to update as expected.
      this.SelectedDataItem = oldItem;
    }
  }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM