hexinnovation / MathConverter

An All-in-One XAML Converter
MIT License
100 stars 17 forks source link

MultiBinding produces exception in Convertor while Binding not #7

Open shcholeh opened 1 year ago

shcholeh commented 1 year ago

Why does MultiBinding with a Converter not work within a ToolTip

Same story was observed by me while using in ContextMenu. Exception raises after click in Menu when ContextMenu starts to close and loses PlacementTarget or its DataContext. If use Binding it has no impact but for MultiBinding yes.

<MenuItem Command="{x:Static local:DBCommands.DoRequery}" Icon="{DynamicResource IconRequery}" >
            <MenuItem.Visibility>
                <MultiBinding Converter="{math:MathConverter}" ConverterParameter="x || y ? `Visible` : `Collapsed`" >
                    <Binding Path="PlacementTarget.WhereQueryVisible" RelativeSource="{RelativeSource AncestorType=ContextMenu}"/>
                    <Binding Path="PlacementTarget.SelectQueryVisible" RelativeSource="{RelativeSource AncestorType=ContextMenu}"/>
                </MultiBinding>
            </MenuItem.Visibility>
        </MenuItem>

Here is the exception:

MathConverter threw an exception while performing a conversion. System.InvalidOperationException: Cannot apply operator '?:' when the first operand is null ConverterParameter: x || y ? `Visible` : `Collapsed` BindingValues: [0]: (MS.Internal.NamedObject): {DependencyProperty.UnsetValue} [1]: (MS.Internal.NamedObject): {DependencyProperty.UnsetValue}

Workaround with combined DependencyProperty in order to use Binding works nice.

<MenuItem Command="{x:Static local:DBCommands.DoRequery}" Icon="{DynamicResource IconRequery}"
                  Visibility="{Binding PlacementTarget.WhereQueryOrSelectQueryVisible, RelativeSource={RelativeSource AncestorType=ContextMenu}, Converter={math:MathConverter}, ConverterParameter='x ? `Visible` : `Collapsed`'}"/>

The use of CustomFunction TryCatch solved my case now.

        <MenuItem Command="{x:Static local:DBCommands.DoRequery}" Icon="{DynamicResource IconRequery}"
                  Visibility="{math:Convert 'TryCatch(x || y ? `Visible` : `Collapsed`, false)', 
                        x={Binding PlacementTarget.WhereQueryVisible, RelativeSource={RelativeSource AncestorType=ContextMenu}},
                        y={Binding PlacementTarget.SelectQueryVisible, RelativeSource={RelativeSource AncestorType=ContextMenu}}}"/>

But what if in code of MathConverter simply after

var sanitizedValues = values?.Select((v, i) => SanitizeBinding(v, i, values.Length, parameter, targetType)).ToArray();

add check for DependencyProperty.UnsetValue among sanitizedValues and return Binding.DoNothing. Or TryCatch was implemented for such cases and is more universal for use? Thanks in advance.

timothylcooke commented 1 year ago

@shcholeh Sorry for the late response here. I didn't see this issue until now.

In WPF (and likely other platforms?), Binding and MultiBinding work differently in the case where the value(s) are UnsetValue.

You can certainly use TryCatch in this scenario, but catching exceptions is very inefficient, so I would recommend against using TryCatch unless absolutely necessary (e.g. if you're trying to convert a user-input value to float, and you don't want to crash the application if the user types something that's not a number).

The SanitizeBinding method automatically converts UnsetValue to null (unless you've set AllowUnsetValue to true on the MathConverter). So, you're likely evaluating the expression null || null ? `Visible` : `Collapsed`, and that's throwing an exception. I would try something like ((x || y) ?? false) ? `Visible` : `Collapsed` for your ConverterParameter to get rid of the exception and to give it a fallback value of Collapsed if both of the bindings cannot return a value.

shcholeh commented 1 year ago

Thank you for explanations and nice link that I added to my collection - not always find nice answers when search the web.

'(x || y) ?? false' solved my case too - seem to perform faster than catching exceptions in debug mode. However observed a number of Encountered UnsetValue in the 1st argument while trying to convert to type "System.Windows.Visibility" using the ConverterParameter "((x || y) ?? false) ? Visible : Collapsed". Double-check that your binding is correct. Encountered UnsetValue in the 2nd argument while trying to convert to type "System.Windows.Visibility" using the ConverterParameter "((x || y) ?? false) ? Visible : Collapsed". Double-check that your binding is correct. in output of VS after normal program termination, hope they are just for information. I made before a number of trials with FallbackValue and TargetNullValue experiments that failed with MultiBinding before I understood that it is not simple. Here I just wanted clean up messages and tried to set FallbackValue once more. With FallbackValue=False I got same list and normal operation. Never knew that in MultiBinding should set in childs FallbackValue expected result in Target. So set FallbackValue=Collapsed for each child and got suddenly exception:

A System.InvalidOperationException was thrown while evaluating the OrNode: (x || y) ---&gt; System.InvalidOperationException: Cannot apply operator '||' to operands of type 'System.Windows.Visibility' and 'System.Windows.Visibility' MathConverter threw an exception while performing a conversion. ConverterParameter: ((x || y) ?? false) ? `Visible` : `Collapsed` BindingValues: [0]: (System.Windows.Visibility): Collapsed [1]: (System.Windows.Visibility): Collapsed

FallbackValue values didn't bypass Converter directly to Target but should, or I've misunderstood something. Additional FallbackValue in Converter doesn't help. FallbackValue=false, FallbackValue='false' and [sys:Boolean x:Key="FalseValue"]False [sys:Boolean] with FallbackValue={StaticResource FalseValue} tried also - not seen at all but Visibility passed in a glance.

Here, for example: Text="{math:Convert '$hex.: {x}, dec.: {y}', x={Binding XPath=@code, FallbackValue=''}, y={Binding XPath=@dec, FallbackValue=''}}" I get rid of debug messages where UnsetValue values passed from Binding when DataContext is not yet set. But empty string here is indistinguishable from null and doesn't harm at all, at least no operations here - just ToString().

Sorry if bother you too much ;) Just want to help a bit and get help too