x:Bind Converter and FallbackValue not collaborating (UWP 10)
Asked Answered
T

2

7

I have a problem that involves a bunch of code, but I've isolated it down. If you want a TL;DR; jump to it further down. If you want a bit of context, here's my situation:

I have created three data converters for my bindings. One of them is a "string prefixer": it prefixes whatever you put in with a fixed string. In the current example, that fixed string is "ms-appx:///cache/". The second one turns a string type into an ImageSource, and the third one chains multiple converters together.

I've then created a Xaml resource which is called LocalCacheFile. Everything works as you would think. Xaml code for this looks like so:

<Image Source="{x:Bind imageSource,Converter={StaticResource LocalCacheFile}}" />

However, I'm having the following problem. If I try to use the FallbackValue to put a placeholder image for when imageSource is empty, I get weird behaviour in x:Bind only.

The following code works as one would expect:

<Image Source="{Binding imageSource,FallbackValue='ms-appx:///Assets/default.png',Converter={StaticResource LocalCacheFile}}" />

But

<Image Source="{x:Bind imageSource,FallbackValue='ms-appx:///Assets/default.png',Converter={StaticResource LocalCacheFile}}" />

does not!

I've isolated it down to just one converter and it is DependencyProperty.UnsetValue that x:Bind seems not to be handling.

TL;DR; Here is the code for my string prefixer, which if I use alone as a test triggers the same faulty behaviour:

public class StringPrefix : IValueConverter
{
    public string prefix { get; set; }

    public object Convert(object value, Type typeName, object parameter, string language)
    {
        if (value == DependencyProperty.UnsetValue || value == null || (string)value == "")
            return DependencyProperty.UnsetValue ;

        return (prefix + value.ToString());
    }

    public object ConvertBack(object value, Type typeName, object parameter, string language)
    {
        throw new NotImplementedException();
    }
}

The above converter works as you would expect it to (i.e. if the input string is empty, the fallback value is properly used) when using Binding. It raises a type exception when used with x:Bind.

What's up with this?


Edit: details about the exception.

This is the generated code:

        private void Update_project_imageSource(global::System.String obj, int phase)
        {
            if((phase & ((1 << 0) | NOT_PHASED | DATA_CHANGED)) != 0)
            {
                XamlBindingSetters.Set_Windows_UI_Xaml_Controls_Image_Source(this.obj16, (global::Windows.UI.Xaml.Media.ImageSource)this.LookupConverter("LocalCacheFile").Convert(obj, typeof(global::Windows.UI.Xaml.Media.ImageSource), null, null), null);
            }
        }

Exception details:

System.InvalidCastException was unhandled by user code
  HResult=-2147467262
  Message=Unable to cast object of type 'System.__ComObject' to type 'Windows.UI.Xaml.Media.ImageSource'.
  Source=Test
  StackTrace:
       at Test.Pages.ProjectView.ProjectView_obj1_Bindings.Update_project_imageSource(String obj, Int32 phase)
       at Test.Pages.ProjectView.ProjectView_obj1_Bindings.Update_project(Project obj, Int32 phase)
       at Test.Pages.ProjectView.ProjectView_obj1_Bindings.Update_(ProjectView obj, Int32 phase)
       at Test.Pages.ProjectView.ProjectView_obj1_Bindings.Update()
       at Test.Pages.ProjectView.<.ctor>b__6_0(FrameworkElement s, DataContextChangedEventArgs e)
  InnerException: 

(to me, it looks like the generated code just doesn't deal with the default value possibility. Btw, that __ComObject is the DependencyProperty.UnsetValue.

Edit 2: I should add that if I change the Convert function to return null instead of DependencyProperty.UnsetValue, x:Bind functions, but then neither x:Bind nor Binding do their expected job of using the FallbackValue

Theophrastus answered 4/2, 2016 at 12:18 Comment(5)
Could you add the details about the TypeException?Yungyunick
I've added the exception info.Theophrastus
Not as solution, but as workaround: How about using the paramater to provide the fallback value, rather than the actual FallbackValue of the binding itself?Yungyunick
Yes, I've tried that too. There's 2 advantages to FallbackValue: runtime, and design time benefit. In the end, I've just opted to kludge around the runtime solution (i.e. by having a placeholder image be copied) especially given that I don't foresee any scaling issues with my data model. That said, I think this is a genuine bug. Another annoyance with x:Bind (the reason why I had to chain converters): once you convert, type coercion is lost. You can bind to a Source attribute with the data being of string type, but you can't if a converter outputs a string. It's annoying.Theophrastus
I'd agree that this is a bug. But I'm kinda curious, why the hell there's a COM_Object... Make sure to report this bug to github.com/dotnet/roslyn ! If any response is created there, make sure to update this question as well.Yungyunick
S
9

The FallbackValue in Binding and x:Bind is different.

In Binding, FallbackValue is the value to use when the binding is unable to return a value.

A binding uses FallbackValue for cases where the Path doesn't evaluate on the data source at all, or if attempting to set it on the source with a two-way binding throws an exception that's caught by the data binding engine. FallbackValue is also used if the source value is the dependency property sentinel value DependencyProperty.UnsetValue.

But in x:Bind, FallbackValue specifies a value to display when the source or path cannot be resolved. It can't work with DependencyProperty.UnsetValue.

As you've already know, x:Bind generates code at compile-time and it's strongly typed. When you use Converter in x:Bind, it will regard the Converter's return value of the same type as the target property and cast it like in your code:

(global::Windows.UI.Xaml.Media.ImageSource)this.LookupConverter("LocalCacheFile").Convert(obj, typeof(global::Windows.UI.Xaml.Media.ImageSource), null, null)

If you return DependencyProperty.UnsetValue in your Converter, it will throw exception as DependencyProperty.UnsetValue can't cast to ImageSource.

For your scenario, you can use TargetNullValue.

TargetNullValue is a similar property with similar scenarios. The difference is that a binding uses TargetNullValue if the Path and Source do evaluate, but the value found there is null.

For example using following code is XAML.

<Image Source="{x:Bind imageSource, TargetNullValue='ms-appx:///Assets/default.png', Converter={StaticResource LocalCacheFile}}" />

And in the Convert, return null instead of DependencyProperty.UnsetValue.

This works when running the app and the imageSource is empty. But to gain design time benefit, we still need use FallbackValue. So we can use x:Bind like following:

<Image Source="{x:Bind imageSource, TargetNullValue='ms-appx:///Assets/default.png', FallbackValue='ms-appx:///Assets/default.png', Converter={StaticResource LocalCacheFile}}" />
Simplicity answered 5/2, 2016 at 7:42 Comment(2)
Thanks, this solution is great! I don't know how it happened, but I didn't see it until a year after. I've marked it as solved, sorry about the delay!Theophrastus
This is a really good example of why in UWP we DO NOT return UnsetValue from converters!Saxena
T
3

In x:Bind the FallBackValue is really only used for designtime data. Now, let's talk about something more important. Why use x:Bind. With the cost of spinning up an IValueConverter, are you convinced x:Bind is worth it? I'm not. When I see developers struggling to get x:Bind to work right for bindings OUTSIDE of a list, my recommendation is to switch to binding. Every time. Inside a list, compiled binding has a "repeat" value, but anywhere else, you have to prove to me that it is worth the effort - if it is otherwise difficult. Typically x:bind is great. But in cases like this, and cases like UpdateSourceTrigger falling back to or defaulting to binding is perfectly fine.

Tilghman answered 7/2, 2016 at 6:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.