【问题标题】:Binding to dependencyproperty in user control and calling CanExecute on nested property change绑定到用户控件中的依赖属性并在嵌套属性更改时调用 CanExecute
【发布时间】:2021-12-30 08:27:18
【问题描述】:

我创建了一个用户控件,它基本上包含一堆组合框,称为 SearchParamsControl。 SearchParamsControl 包含在 SearchParams 类中设置所有内容所需的所有 UI 元素:

我的 SearchParamsControl:

    /// <summary>
/// Interaction logic for SearchParamsControl.xaml
/// </summary>
public partial class SearchParamsControl : INotifyPropertyChanged
{
    public SearchParamsControl()
    {
        InitializeComponent();
    }

    /// <summary>
    /// Radius entries bound to combobox
    /// </summary>
    public Dictionary<double, string> RadiusEntries
    {
        get;
        set;
    } = new Dictionary<double, string>()
    {
        {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 Dictionary<PropertyTypeEnum, string> PropertyTypes => PropertyTypeDictionary;

    /// <summary>
    /// Prices bound to combo box
    /// </summary>
    public List<int> Prices
    {
        get;
        set;
    } = new List<int>()
    {
        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
    };

    /// <summary>
    /// Bedrooms bound to combobox
    /// </summary>
    public List<int> Bedrooms
    {
        get;
        set;
    } = new List<int>()
    {
        0,
        1,
        2,
        3,
        4,
        5
    };

    public StringTrieSet SearchString
    {
        get
        {
            return RightMoveCodes.RegionTree;
        }
    }

    public double Radius
    {
        get => 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:

<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">
<UserControl.DataContext>
    <viewModel:SearchParamsControlViewModel/>
</UserControl.DataContext>
<UserControl.Resources>
    <ObjectDataProvider x:Key="dataFromEnum" MethodName="GetValues" ObjectType="{x:Type System:Enum}">
        <ObjectDataProvider.MethodParameters>
            <x:Type TypeName="dataTypes:SortType"/>
        </ObjectDataProvider.MethodParameters>
    </ObjectDataProvider>
    <valueconverters:PropertyTypeConverter x:Key="PropertyTypeConverter" x:Shared="False"/>
    <Style TargetType="{x:Type ComboBox}" BasedOn="{StaticResource ComboStyle}">
        <Setter Property="Margin" Value="0,0,0,1"/>
    </Style> 
</UserControl.Resources>
<Grid x:Name="LayoutRoot">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="1*"/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <Label Grid.Column="0" Grid.Row="0" Content="Search area"/>

    <local:AutoCompleteComboBox Grid.Row="0" Grid.Column="1"
                                   ItemsSource="{Binding ElementName=uc, Path=SearchString}" 
                                   SelectedValue="{Binding Path=RegionLocation, Mode=TwoWay}"/>

    <Label Grid.Column="0" Grid.Row="1" Content="Search radius"/>
    <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}">
        <ComboBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding Value}"/>
                </StackPanel>
            </DataTemplate>
        </ComboBox.ItemTemplate>
    </ComboBox>

    <Label Grid.Column="0" Grid.Row="2" Content="Price range (£)"/>
    <Grid Grid.Column="1" Grid.Row="2">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="1*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <ComboBox Grid.Column="0" Grid.Row="0" Name="comboMinPrice" 
                  ItemsSource="{Binding ElementName=uc, Path=Prices}" 
                  SelectedItem="{Binding ElementName=uc, Path=MinPrice, Mode=TwoWay}">
            <ComboBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding}"/>
                    </StackPanel>
                </DataTemplate>
            </ComboBox.ItemTemplate>
        </ComboBox>
        <Label Grid.Column="1" Grid.Row="0" Content="to"/>
        <ComboBox Grid.Column="2" Grid.Row="0" Name="comboMaxPrice"
                  ItemsSource="{Binding ElementName=uc, Path=Prices}" 
                  SelectedItem="{Binding ElementName=uc, Path=MaxPrice}">
            <ComboBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding}"/>
                    </StackPanel>
                </DataTemplate>
            </ComboBox.ItemTemplate>
        </ComboBox>
    </Grid>
    <Label Grid.Column="0" Grid.Row="3" Content="No. of bedrooms"/>
    <Grid Grid.Column="1" Grid.Row="3">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="1*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <ComboBox Grid.Column="0" Grid.Row="0" Name="comboMinBedrooms" 
                  ItemsSource="{Binding ElementName=uc, Path=Bedrooms}"
                  SelectedItem="{Binding ElementName=uc, Path=MinBedrooms, Mode=TwoWay}">
            <ComboBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding}"/>
                    </StackPanel>
                </DataTemplate>
            </ComboBox.ItemTemplate>
        </ComboBox>
        <ComboBox Grid.Column="3" Grid.Row="0" Name="comboMaxBedrooms" 
                  ItemsSource="{Binding ElementName=uc, Path=Bedrooms}" 
                  SelectedItem="{Binding ElementName=uc, Path=MaxBedrooms, Mode=TwoWay}">
            <ComboBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding}"/>
                    </StackPanel>
                </DataTemplate>
            </ComboBox.ItemTemplate>
        </ComboBox>
        <Label Grid.Column="1" Grid.Row="0" Content="to"/>
    </Grid>
    <Label Grid.Column="0" Grid.Row="4" Content="Sort Type"/>

    <ComboBox Grid.Column="1" Grid.Row="4" Name="comboSort" 
                      ItemsSource="{Binding Source={StaticResource dataFromEnum}}"
                      SelectedItem="{Binding ElementName=uc, Path=SortType, Mode=TwoWay}">
    </ComboBox>
</Grid>

我想绑定到我的 MainWindow 中的 SearchParams 依赖属性,其中包含 SearchParamsControl:

<GroupBox Grid.Column="0" Grid.Row="0" Header="Search Params" Panel.ZIndex="10">
    <controls:SearchParamsControl x:Name="searchControl"
                                SearchParams="{Binding Path=SearchParams, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                                IsEnabled="{Binding Path=IsSearching, Converter={StaticResource BooleanToReverseConverter}}">

    </controls:SearchParamsControl>
</GroupBox>
<Button x:Name="btnSearch" Grid.Column="0" Grid.Row="1" 
        Content="Search" 
        IsDefault="True"
        Command="{Binding SearchAsyncCommand}"/>

此绑定工作正常。但是,如您所见,我的 MainWindow 中有一个 Button,它连接到一个命令。我想知道 SearchParams 中的属性何时发生更改,以便我可以调用类似 SearchCommandAsync.RaiseCanExecuteChanged() 的内容,以便更新“搜索”按钮的状态。我该如何处理?

请注意,我不想使用以下注释掉的代码(来自我的 Command 类),我认为它确实有效,但我希望能够通知 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 事件处理程序修改 SearchParams(想象它是一个封闭的类库,我不能修改它)。

【问题讨论】:

    标签: c# wpf data-binding wpf-controls dependency-properties


    【解决方案1】:

    如果我正确理解您的设置,则视图上SearchParamsControlSearchParams 属性应在控件属性更改时设置视图模型的数据绑定SearchParams 源属性。

    然后您可以在SearchParams 源属性的设置器中引发命令的CanExecuteChanged 方法。

    【讨论】:

    • 就是这样,SearchParams 源属性(如果你的意思是我的 MainWindow 的 MainWindowViewModel 中的 SearchParams)设置器永远不会执行,很可能是因为它是 SearchParams.Radius,比如说,它被改变了。但是,如果我强制 CanExecute 为 true,我可以在调试器中检查 MainViewModel 的 SearchParams 并确认 SearchParams 的 Radius 属性已正确更新,但永远不会命中 SearchParams 设置器断点。我什至尝试将 SearchParams=SearchParams 放在 UserControl 中的 Radius 设置中,以尝试强制调用 MainViewModel 中的设置器
    • 所以绑定不起作用?顺便说一句,您应该从控件中删除 &lt;UserControl.DataContext&gt; 元素。控件应该从父元素继承其DataContext
    • 抱歉,我已将其删除(我已完全删除 SearchControlViewModel)。绑定确实有效,SearchParams 的修改属性在 MainWindowViewModel 中是正确的。当我在我的 ExecuteSearch 方法中放置一个断点时,我会调试 SearchParams 并且我的更改会正确显示。但是 SearchParams 本身并没有改变,所以我想这就是为什么不调用 setter 的原因 - 但我需要调用它,这样我才能引发 CanExecuteChanged
    • 那么控件是如何告诉视图模型它已经改变的呢?
    • 这就是我的问题所在。我该怎么做?仅供参考,刚刚尝试放入 Radius 的设置器 SearchParams = new SearchParams(SearchParams) (即创建它的新实例),现在调用 MainWindowViewModel 中的 SearchParams 设置器(我可以在那里提出 CanExecuteChanged )。这绝对看起来是一种 hacky 方法,一定有更好的方法。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多