Привязка к свойству dependencyproperty в пользовательском элементе управления и вызов CanExecute при изменении вложенного свойства

#c# #wpf #data-binding #wpf-controls #dependency-properties

Вопрос:

Я создал пользовательский элемент управления, который, по сути, содержит набор комбинаций, называемых SearchParamsControl. Элемент управления SearchParamsControl содержит все элементы пользовательского интерфейса, необходимые для настройки всего в классе SearchParams:

Мой поискпарамконтроль:

 /// lt;summarygt; /// Interaction logic for SearchParamsControl.xaml /// lt;/summarygt; public partial class SearchParamsControl : INotifyPropertyChanged {  public SearchParamsControl()  {  InitializeComponent();  }   /// lt;summarygt;  /// Radius entries bound to combobox  /// lt;/summarygt;  public Dictionarylt;double, stringgt; RadiusEntries  {  get;  set;  } = new Dictionarylt;double, stringgt;()  {  {0, "This area only" },  { 0.25, "Within 1/4 mile" },  { 0.5, "Within 1/2 mile" },  { 1, "Within 1 mile" },  { 3, "Within 3 miles" },  { 5, "Within 5 miles" },  { 10, "Within 10 miles" },  { 15, "Within 15 miles" },  { 20, "Within 20 miles" },  { 30, "Within 30 miles" },  { 40, "Within 40 miles" }  };   public Dictionarylt;PropertyTypeEnum, stringgt; PropertyTypes =gt; PropertyTypeDictionary;   /// lt;summarygt;  /// Prices bound to combo box  /// lt;/summarygt;  public Listlt;intgt; Prices  {  get;  set;  } = new Listlt;intgt;()  {  0,  50000,  60000,  70000,  80000,  90000,  100000,  110000,  120000,  125000,  130000,  150000,  200000,  250000,  300000,  325000,  375000,  400000,  425000,  450000,  475000,  500000,  550000,  600000,  650000,  700000,  800000,  900000,  1000000,  1250000,  1500000,  1750000,  2000000,  2500000,  3000000,  4000000,  5000000,  7500000,  10000000,  15000000,  20000000  };   /// lt;summarygt;  /// Bedrooms bound to combobox  /// lt;/summarygt;  public Listlt;intgt; Bedrooms  {  get;  set;  } = new Listlt;intgt;()  {  0,  1,  2,  3,  4,  5  };   public StringTrieSet SearchString  {  get  {  return RightMoveCodes.RegionTree;  }  }   public double Radius  {  get =gt; SearchParams.Radius;  set  {  if (SearchParams.Radius != value)  {  SearchParams.Radius = value;  OnSearchParamsChanged();  }  }  }   public int MinBedrooms  {  get { return SearchParams.MinBedrooms; }  set  {  if (SearchParams.MinBedrooms != value)  {  SearchParams.MinBedrooms = value;  OnSearchParamsChanged();  }  }  }   public int MaxBedrooms  {  get { return SearchParams.MaxBedrooms; }  set   {   if (SearchParams.MaxBedrooms != value)  {  SearchParams.MaxBedrooms = value;  OnSearchParamsChanged();  }  }  }   public int MinPrice  {  get { return SearchParams.MinPrice; }  set   {   if (SearchParams.MinPrice != value)   {  SearchParams.MinPrice = value;  OnSearchParamsChanged();  }  }  }   public int MaxPrice  {  get { return SearchParams.MaxPrice; }  set  {  if (SearchParams.MaxPrice != value)  {  SearchParams.MaxPrice = value;  OnSearchParamsChanged();  }  }  }   public SortType SortType  {  get { return SearchParams.Sort; }  set  {  if (SearchParams.Sort != value)  {  SearchParams.Sort = value;  OnSearchParamsChanged();  }  }  }     public SearchParams SearchParams  {  get  {  SearchParams searchParams = (SearchParams)GetValue(SearchParamsProperty);  return searchParams;  }  set  {  SetValue(SearchParamsProperty, value);  }  }   // Using a DependencyProperty as the backing store for MySelectedItem. This enables animation, styling, binding, etc...  public static readonly DependencyProperty SearchParamsProperty =  DependencyProperty.Register("SearchParams", typeof(SearchParams), typeof(SearchParamsControl), new PropertyMetadata(new SearchParams(), OnSearchParamsPropertyChanged));   public event PropertyChangedEventHandler PropertyChanged;   private static void OnSearchParamsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)  {  SearchParamsControl c = d as SearchParamsControl;   if (c != null)  {  c.OnSearchParamsChanged();  }  }   private void OnSearchParamsChanged()  {  OnPropertyChanged(nameof(SearchParams));  }   private void OnPropertyChanged([CallerMemberName] string propertyName = null)  {  PropertyChangedEventHandler handler = PropertyChanged;  if (handler != null)  handler(this, new PropertyChangedEventArgs(propertyName));  } }  

И xml:

 lt;UserControl x:Class="RightMoveApp.UserControls.SearchParamsControl"  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"   xmlns:d="http://schemas.microsoft.com/expression/blend/2008"   xmlns:local="clr-namespace:RightMoveApp.UserControls"  xmlns:System="clr-namespace:System;assembly=System.Runtime"  xmlns:StyleAlias="clr-namespace:RightMove;assembly=RightMove"  xmlns:viewModel="clr-namespace:RightMoveApp.ViewModel"  xmlns:dataTypes="clr-namespace:RightMove.DataTypes;assembly=RightMove"   xmlns:valueconverters="clr-namespace:RightMoveApp.UserControls.ValueConverters"  mc:Ignorable="d"   d:DesignHeight="450" d:DesignWidth="800"  x:Name="uc"gt; lt;UserControl.DataContextgt;  lt;viewModel:SearchParamsControlViewModel/gt; lt;/UserControl.DataContextgt; lt;UserControl.Resourcesgt;  lt;ObjectDataProvider x:Key="dataFromEnum" MethodName="GetValues" ObjectType="{x:Type System:Enum}"gt;  lt;ObjectDataProvider.MethodParametersgt;  lt;x:Type TypeName="dataTypes:SortType"/gt;  lt;/ObjectDataProvider.MethodParametersgt;  lt;/ObjectDataProvidergt;  lt;valueconverters:PropertyTypeConverter x:Key="PropertyTypeConverter" x:Shared="False"/gt;  lt;Style TargetType="{x:Type ComboBox}" BasedOn="{StaticResource ComboStyle}"gt;  lt;Setter Property="Margin" Value="0,0,0,1"/gt;  lt;/Stylegt;  lt;/UserControl.Resourcesgt; lt;Grid x:Name="LayoutRoot"gt;  lt;Grid.ColumnDefinitionsgt;  lt;ColumnDefinition Width="Auto"/gt;  lt;ColumnDefinition Width="1*"/gt;  lt;/Grid.ColumnDefinitionsgt;  lt;Grid.RowDefinitionsgt;  lt;RowDefinition Height="Auto"/gt;  lt;RowDefinition Height="Auto"/gt;  lt;RowDefinition Height="Auto"/gt;  lt;RowDefinition Height="Auto"/gt;  lt;RowDefinition Height="Auto"/gt;  lt;RowDefinition Height="Auto"/gt;  lt;/Grid.RowDefinitionsgt;  lt;Label Grid.Column="0" Grid.Row="0" Content="Search area"/gt;   lt;local:AutoCompleteComboBox Grid.Row="0" Grid.Column="1"  ItemsSource="{Binding ElementName=uc, Path=SearchString}"   SelectedValue="{Binding Path=RegionLocation, Mode=TwoWay}"/gt;   lt;Label Grid.Column="0" Grid.Row="1" Content="Search radius"/gt;  lt;ComboBox Template="{DynamicResource ComboBoxTemplate1}" Grid.Column="1" Grid.Row="1" Name="comboSearchRadius"   ItemsSource="{Binding ElementName=uc, Path=RadiusEntries}"   SelectedValuePath="Key"   SelectedValue="{Binding ElementName=uc, Path=Radius, Mode=TwoWay}"gt;  lt;ComboBox.ItemTemplategt;  lt;DataTemplategt;  lt;StackPanel Orientation="Horizontal"gt;  lt;TextBlock Text="{Binding Value}"/gt;  lt;/StackPanelgt;  lt;/DataTemplategt;  lt;/ComboBox.ItemTemplategt;  lt;/ComboBoxgt;   lt;Label Grid.Column="0" Grid.Row="2" Content="Price range (£)"/gt;  lt;Grid Grid.Column="1" Grid.Row="2"gt;  lt;Grid.ColumnDefinitionsgt;  lt;ColumnDefinition Width="1*"/gt;  lt;ColumnDefinition Width="Auto"/gt;  lt;ColumnDefinition Width="1*"/gt;  lt;/Grid.ColumnDefinitionsgt;  lt;Grid.RowDefinitionsgt;  lt;RowDefinition Height="Auto"/gt;  lt;/Grid.RowDefinitionsgt;  lt;ComboBox Grid.Column="0" Grid.Row="0" Name="comboMinPrice"   ItemsSource="{Binding ElementName=uc, Path=Prices}"   SelectedItem="{Binding ElementName=uc, Path=MinPrice, Mode=TwoWay}"gt;  lt;ComboBox.ItemTemplategt;  lt;DataTemplategt;  lt;StackPanel Orientation="Horizontal"gt;  lt;TextBlock Text="{Binding}"/gt;  lt;/StackPanelgt;  lt;/DataTemplategt;  lt;/ComboBox.ItemTemplategt;  lt;/ComboBoxgt;  lt;Label Grid.Column="1" Grid.Row="0" Content="to"/gt;  lt;ComboBox Grid.Column="2" Grid.Row="0" Name="comboMaxPrice"  ItemsSource="{Binding ElementName=uc, Path=Prices}"   SelectedItem="{Binding ElementName=uc, Path=MaxPrice}"gt;  lt;ComboBox.ItemTemplategt;  lt;DataTemplategt;  lt;StackPanel Orientation="Horizontal"gt;  lt;TextBlock Text="{Binding}"/gt;  lt;/StackPanelgt;  lt;/DataTemplategt;  lt;/ComboBox.ItemTemplategt;  lt;/ComboBoxgt;  lt;/Gridgt;  lt;Label Grid.Column="0" Grid.Row="3" Content="No. of bedrooms"/gt;  lt;Grid Grid.Column="1" Grid.Row="3"gt;  lt;Grid.ColumnDefinitionsgt;  lt;ColumnDefinition Width="1*"/gt;  lt;ColumnDefinition Width="Auto"/gt;  lt;ColumnDefinition Width="1*"/gt;  lt;/Grid.ColumnDefinitionsgt;  lt;Grid.RowDefinitionsgt;  lt;RowDefinition Height="Auto"/gt;  lt;/Grid.RowDefinitionsgt;  lt;ComboBox Grid.Column="0" Grid.Row="0" Name="comboMinBedrooms"   ItemsSource="{Binding ElementName=uc, Path=Bedrooms}"  SelectedItem="{Binding ElementName=uc, Path=MinBedrooms, Mode=TwoWay}"gt;  lt;ComboBox.ItemTemplategt;  lt;DataTemplategt;  lt;StackPanel Orientation="Horizontal"gt;  lt;TextBlock Text="{Binding}"/gt;  lt;/StackPanelgt;  lt;/DataTemplategt;  lt;/ComboBox.ItemTemplategt;  lt;/ComboBoxgt;  lt;ComboBox Grid.Column="3" Grid.Row="0" Name="comboMaxBedrooms"   ItemsSource="{Binding ElementName=uc, Path=Bedrooms}"   SelectedItem="{Binding ElementName=uc, Path=MaxBedrooms, Mode=TwoWay}"gt;  lt;ComboBox.ItemTemplategt;  lt;DataTemplategt;  lt;StackPanel Orientation="Horizontal"gt;  lt;TextBlock Text="{Binding}"/gt;  lt;/StackPanelgt;  lt;/DataTemplategt;  lt;/ComboBox.ItemTemplategt;  lt;/ComboBoxgt;  lt;Label Grid.Column="1" Grid.Row="0" Content="to"/gt;  lt;/Gridgt;  lt;Label Grid.Column="0" Grid.Row="4" Content="Sort Type"/gt;   lt;ComboBox Grid.Column="1" Grid.Row="4" Name="comboSort"   ItemsSource="{Binding Source={StaticResource dataFromEnum}}"  SelectedItem="{Binding ElementName=uc, Path=SortType, Mode=TwoWay}"gt;  lt;/ComboBoxgt; lt;/Gridgt;  

I want to bind to the SearchParams dependency property in my MainWindow, which contains the SearchParamsControl:

 lt;GroupBox Grid.Column="0" Grid.Row="0" Header="Search Params" Panel.ZIndex="10"gt;  lt;controls:SearchParamsControl x:Name="searchControl"  SearchParams="{Binding Path=SearchParams, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"  IsEnabled="{Binding Path=IsSearching, Converter={StaticResource BooleanToReverseConverter}}"gt;   lt;/controls:SearchParamsControlgt; lt;/GroupBoxgt; lt;Button x:Name="btnSearch" Grid.Column="0" Grid.Row="1"   Content="Search"   IsDefault="True"  Command="{Binding SearchAsyncCommand}"/gt;  

Эта привязка работает нормально. Но, как вы можете видеть, у меня в главном окне есть кнопка, которая подключена к команде. Я хочу знать, когда изменилось свойство в SearchParams, чтобы я мог вызвать что-то вроде SearchCommandAsync.RaiseCanExecuteChanged (), чтобы я мог обновить состояние кнопки поиска. Как я могу с этим справиться?

Обратите внимание, что я не хочу использовать следующий закомментированный код (из моего командного класса), который, как мне кажется, работает, но я скорее хочу иметь возможность уведомлять, что у SearchParams изменено свойство, и поэтому нам нужно снова вызвать CanExecute:

 //public event EventHandler CanExecuteChanged  //{  // add  // {  // CommandManager.RequerySuggested  = value;  // }  // remove  // {  // CommandManager.RequerySuggested -= value;  // }  //}   public event EventHandler CanExecuteChanged;   public void RaiseCanExecuteChanged()  {  //CommandManager.InvalidateRequerySuggested();  if (CanExecuteChanged != null)  {  CanExecuteChanged(this, new EventArgs());  }  }  

Кроме того, я не хочу изменять параметры поиска с помощью обработчика событий PropertyChanged (представьте, что это закрытая библиотека классов, и я не могу ее изменить).

Ответ №1:

Если я правильно понимаю вашу настройку, SearchParams свойство SearchParamsControl в представлении должно устанавливать свойство SearchParams источника с привязкой к данным модели представления при каждом изменении свойства элемента управления.

Затем вы можете вызвать CanExecuteChanged метод команды в установщике SearchParams исходного свойства.

Комментарии:

1. В том-то и дело, что свойство источника SearchParams (если вы имеете в виду параметры поиска в модели mainwindowviewмодели моего главного окна) никогда не выполняется, скорее всего, потому, что это параметры поиска. Радиус, скажем, который изменен. Однако, если я принудительно установлю значение CanExecute в значение true, я могу проверить параметры поиска в модели mainviewмодели в отладчике и подтвердить, что свойство Radius SearchParams правильно обновлено, но точка останова установщика параметров поиска никогда не будет достигнута. Я даже попытался ввести параметры поиска=параметры поиска в настройках Radius в пользовательском контроле, чтобы попытаться принудительно вызвать сеттер в MainViewModel

2. Значит, привязка не работает? Кстати, вам следует удалить lt;UserControl.DataContextgt; элемент из элемента управления. Элемент управления должен наследовать его DataContext от родительского элемента.

3. Мои извинения, я удалил это (я полностью удалил SearchControlViewModel). Привязка работает, измененные свойства параметров поиска верны в модели MainWindowViewModel. Когда я устанавливаю точку останова в своем методе ExecuteSearch, я отлаживаю параметры поиска, и мои изменения отображаются правильно. Но сам SearchParams не изменился, поэтому, я думаю, именно поэтому сеттер не вызывается — но мне нужно, чтобы он был вызван, чтобы я мог вызвать CanExecuteChanged

4. Так как же элемент управления сообщает модели представления, что она изменилась?

5. И в этом-то и заключается моя проблема. Как мне это сделать? К вашему сведению, только что попробовал ввести сеттер Radius, SearchParams = новые параметры поиска(параметры поиска) (т. Е. Создать его новый экземпляр), и теперь вызывается мой сеттер параметров поиска в MainWindowViewModel (и я могу вызвать CanExecuteChanged там). Это определенно кажется банальным подходом, должен быть лучший способ.