microsoft / XamlBehaviorsWpf

Home for WPF XAML Behaviors on GitHub.
MIT License
839 stars 138 forks source link

A binding error is reported about CommandParameter #147

Closed guchen66 closed 2 weeks ago

guchen66 commented 8 months ago

我在使用xmlns:i="http://schemas.microsoft.com/xaml/behaviors"时候报错, 我想要实现的效果是让整个页面的TextBox都是只读状态,所以我遍历整个视觉树,找到TextBox,设置为只读。 下面是一个很简单的例子


        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:prism="http://prismlibrary.com/"
        prism:ViewModelLocator.AutoWireViewModel="True"
        xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
       Height="350" Width="525" >
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Loaded">
            <i:InvokeCommandAction Command="{Binding BtnCommand}" 
                                   CommandParameter="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <Grid>
        <StackPanel>
            <TextBox Width="200" Text="200" Height="20" Margin="10"/>
            <Button Width="200" Height="20" Command="{Binding BtnCommand}" CommandParameter="{Binding RelativeSource={RelativeSource AncestorType={x:Type StackPanel}}}"/>
        </StackPanel>
    </Grid>
</Window>
 public class MainWindowViewModel : BindableBase
 {
     public ICommand BtnCommand { get; set; }
     public MainWindowViewModel()
     {
         BtnCommand = new DelegateCommand<DependencyObject>(ExecuteBtnCommand);
     }
     private void ExecuteBtnCommand(DependencyObject obj)
     {
         SetTextBoxesReadOnly(obj,true);
     }

     public void SetTextBoxesReadOnly(DependencyObject parent, bool isReadOnly)
     {
         for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
         {
             var child = VisualTreeHelper.GetChild(parent, i);
             if (child is TextBox textBox)
             {
                 textBox.IsReadOnly = isReadOnly;
             }
             else
             {
                 SetTextBoxesReadOnly(child, isReadOnly);
             }
         }
     }
 }```
这样写是成功的,但是当我删除<Button Width="200" Height="20" Command="{Binding BtnCommand}" CommandParameter="{Binding RelativeSource={RelativeSource AncestorType={x:Type StackPanel}}}"/>里面的关于CommandParameter的绑定,就会报错System.ArgumentNullException:“Value cannot be null. Arg_ParamName_Name”
brianlagunas commented 8 months ago

Please provide a reproduction app (not a code snippet), or this issue will be closed.

LWChris commented 4 months ago

Allow me to translate:

I get an error message when I use xmlns:i="http://schemas.microsoft.com/xaml/behaviors". What I want to do is make every TextBox of the whole page read-only, so I go through the whole visual tree and when I find a TextBox set it to read-only. Here is a very simple example:

<Window x:Class="XamlBehaviorsTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:prism="http://prismlibrary.com/"
        prism:ViewModelLocator.AutoWireViewModel="True"
        xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
        Height="350" Width="525" >
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Loaded">
            <i:InvokeCommandAction Command="{Binding BtnCommand}" 
                                   CommandParameter="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <Grid>
        <StackPanel>
            <TextBox Width="200" Text="200" Height="20" Margin="10"/>
            <Button Width="200" Height="20" Command="{Binding BtnCommand}" CommandParameter="{Binding RelativeSource={RelativeSource AncestorType={x:Type StackPanel}}}"/>
        </StackPanel>
    </Grid>
</Window>
 public class MainWindowViewModel : BindableBase
 {
     public ICommand BtnCommand { get; set; }
     public MainWindowViewModel()
     {
         BtnCommand = new DelegateCommand<DependencyObject>(ExecuteBtnCommand);
     }
     private void ExecuteBtnCommand(DependencyObject obj)
     {
         SetTextBoxesReadOnly(obj,true);
     }

     public void SetTextBoxesReadOnly(DependencyObject parent, bool isReadOnly)
     {
         for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
         {
             var child = VisualTreeHelper.GetChild(parent, i);
             if (child is TextBox textBox)
             {
                 textBox.IsReadOnly = isReadOnly;
             }
             else
             {
                 SetTextBoxesReadOnly(child, isReadOnly);
             }
         }
     }
 }

Writing it like this works, but when I remove the CommandParameter attribute from the <Button Width="200" Height="20" Command="{Binding BtnCommand}" CommandParameter="{Binding RelativeSource={RelativeSource AncestorType={x:Type StackPanel}}}"/> I get a System.ArgumentNullException: "Value cannot be null. Arg_ParamName_Name".

The error:

So in a nutshell: removing the CommandParameter attribute of a Button causes an unrelated InvokeCommandAction to no longer evaluate/recognize a CommandParameter.

I have created a test project, and tried the following:

brianlagunas commented 2 weeks ago

The WPF binding engine might be optimizing or delaying certain bindings until it encounters a scenario where it needs to resolve them. Setting the CommandParameter on the Button could be causing the binding engine to resolve all bindings within that context earlier or more thoroughly, including those in the InvokeCommandAction.

In general, I would recommend avoiding RelativeSource bindings if possible and just use the ElementName instead.

image

LWChris commented 1 week ago

@brianlagunas But does using ElementName fix the issue, or is it related to https://github.com/dotnet/wpf/issues/316 / https://github.com/dotnet/wpf/pull/4217 ?