Reputation: 60506
I have the following converter:
public class MyConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
Debug.WriteLine(value.GetType());
//The rest of the code
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
And the XAML that attempts to use the converter:
<ListView ItemsSource="{x:Bind StickersCVS.View}" >
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:StickerCategory">
<TextBlock Foreground="{x:Bind Converter={StaticResource MyConverter}}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
This gives me an NPE at value.GetType()
, apparently the value passed in is null
.
If I change the following part:
<TextBlock Foreground="{x:Bind Converter={StaticResource MyConverter}}"/>
to
<TextBlock Foreground="{Binding Converter={StaticResource MyConverter}}"/>
Then it works. The Debug
correctly outputs StickerCategory
as the type of the value. Any reason why x:Bind
passes null
into the converter and how do I make it work with x:Bind
? I'm trying to pass DataContext
to my converter.
Upvotes: 3
Views: 6569
Reputation: 10214
However, since version 1607 you can use functions with x:Bind
and this opens up new opportunities: quick conversion without the added complexity of converters. See the documentation.
Upvotes: 0
Reputation: 1386
I don't think it is a good idea to use x:bind with converter in general, the goal of using x:bing is to increase performance, while converter will dramatically affect code's performance. you can easily create a readonly field in your model, and when your data is changed, you can raise property changed event, and convert the number there.
For example,
[JsonIgnore]
public double IsUnreadOpacity
{
get { return (IsUnread) ? 1 : 0; }
}
public bool IsUnread
{
get { return _isUnread; }
set
{
if (value == _isUnread)
return;
Set(ref _isUnread, value);
RaisePropertyChanged(nameof(IsUnreadOpacity));
}
}
Upvotes: 1
Reputation: 15758
{x:Bind}
uses generated code to achieve its benefits and while using different Path
in {x:Bind}
, the generated code has some differences.
Here I use a simple sample for example. For the complete sample, please check at GitHub.
In the sample, I have a ViewModel like following:
public class MyViewModel
{
public MyViewModel()
{
MyList = new List<Item>()
{
new Item {Name="1",Number=1 },
new Item {Name="2",Number=2 },
new Item {Name="3",Number=3 }
};
}
public List<Item> MyList { get; set; }
}
public class Item
{
public string Name { get; set; }
public int Number { get; set; }
public override string ToString()
{
return string.Format("Name: {0}, Number {1}", this.Name, this.Number);
}
}
When we use {x:Bind Name, Converter={StaticResource ItemConvert}}
in MainPage.xaml
<ListView ItemsSource="{x:Bind ViewModel.MyList}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:Item">
<TextBlock Text="{x:Bind Converter={StaticResource ItemConvert}}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
It generates following code in MainPage.g.cs
public void DataContextChangedHandler(global::Windows.UI.Xaml.FrameworkElement sender, global::Windows.UI.Xaml.DataContextChangedEventArgs args)
{
global::xBindWithConverter.Item data = args.NewValue as global::xBindWithConverter.Item;
if (args.NewValue != null && data == null)
{
throw new global::System.ArgumentException("Incorrect type passed into template. Based on the x:DataType global::xBindWithConverter.Item was expected.");
}
this.SetDataRoot(data);
this.Update();
}
// IDataTemplateExtension
public bool ProcessBinding(uint phase)
{
throw new global::System.NotImplementedException();
}
public int ProcessBindings(global::Windows.UI.Xaml.Controls.ContainerContentChangingEventArgs args)
{
int nextPhase = -1;
switch(args.Phase)
{
case 0:
nextPhase = -1;
this.SetDataRoot(args.Item as global::xBindWithConverter.Item);
if (!removedDataContextHandler)
{
removedDataContextHandler = true;
((global::Windows.UI.Xaml.Controls.TextBlock)args.ItemContainer.ContentTemplateRoot).DataContextChanged -= this.DataContextChangedHandler;
}
this.initialized = true;
break;
}
this.Update_((global::xBindWithConverter.Item) args.Item, 1 << (int)args.Phase);
return nextPhase;
}
...
public void Update()
{
this.Update_(this.dataRoot, NOT_PHASED);
this.initialized = true;
}
And
global::Windows.UI.Xaml.Controls.TextBlock element3 = (global::Windows.UI.Xaml.Controls.TextBlock)target;
MainPage_obj3_Bindings bindings = new MainPage_obj3_Bindings();
returnValue = bindings;
bindings.SetDataRoot((global::xBindWithConverter.Item) element3.DataContext);
bindings.SetConverterLookupRoot(this);
element3.DataContextChanged += bindings.DataContextChangedHandler;
global::Windows.UI.Xaml.DataTemplate.SetExtensionInstance(element3, bindings);
When initializing the Page, element3.DataContextChanged += bindings.DataContextChangedHandler;
will be executed firstly. After this, DataContextChangedHandler
method will be called as DataContextChanged
event is raised while initializing. And the ProcessBindings
method will be executed to update list item container element with bound data.
In the DataContextChangedHandler
method, it calls this.Update();
method which calls Update_(global::xBindWithConverter.Item obj, int phase)
method in the end. But when the DataContextChangedHandler
method is called, it args.NewValue
value is null
, so the obj
in Update_(global::xBindWithConverter.Item obj, int phase)
method is also null
.
And when using {x:Bind Converter={StaticResource ItemConvert}}
in XAML, the generated code for Update_(global::xBindWithConverter.Item obj, int phase)
is:
// Update methods for each path node used in binding steps.
private void Update_(global::xBindWithConverter.Item obj, int phase)
{
if((phase & ((1 << 0) | NOT_PHASED )) != 0)
{
XamlBindingSetters.Set_Windows_UI_Xaml_Controls_TextBlock_Text(this.obj3.Target as global::Windows.UI.Xaml.Controls.TextBlock, (global::System.String)this.LookupConverter("ItemConvert").Convert(obj, typeof(global::System.String), null, null), null);
}
}
As the obj
is null
, so the value
in your Convert
is null
and finally it throws a NPE at value.GetType()
.
But if we use another Path
in {x:Bind}
like {x:Bind Name, Converter={StaticResource ItemConvert}}
, the generated code for Update_(global::xBindWithConverter.Item obj, int phase)
is different:
// Update methods for each path node used in binding steps.
private void Update_(global::xBindWithConverter.Item obj, int phase)
{
if (obj != null)
{
if ((phase & (NOT_PHASED | (1 << 0))) != 0)
{
this.Update_Name(obj.Name, phase);
}
}
}
private void Update_Name(global::System.String obj, int phase)
{
if((phase & ((1 << 0) | NOT_PHASED )) != 0)
{
XamlBindingSetters.Set_Windows_UI_Xaml_Controls_TextBlock_Text(this.obj3.Target as global::Windows.UI.Xaml.Controls.TextBlock, (global::System.String)this.LookupConverter("ItemConvert").Convert(obj, typeof(global::System.String), null, null), null);
}
}
It will determine whether the obj
is null
. So the XamlBindingSetters.Set_Windows_UI_Xaml_Controls_TextBlock_Text
method won't be called here and the NullReferenceException
won't occur.
To solve the problem, just like @Markus Hütter said, you can add a null
check in your converter like:
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value != null)
{
System.Diagnostics.Debug.WriteLine(value.GetType());
return value.ToString();
}
else
{
System.Diagnostics.Debug.WriteLine("value is null");
return null;
}
}
Upvotes: 2