XAML converter binding for multiple properties - xaml

I want to display some text in a Xamarin forms ListView grid, based on the comparison of 2 date properties. I have a converter to compare the dates and return a string. What I am trying to do (without success) is pass the entire object to the converter.
XAML:
<ResourceDictionary>
<converters1:CancelConverter x:Key="CancelConverter" />
</ResourceDictionary>
...
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Grid>
<Label Grid.Column="0" Text="{Binding ., Converter={StaticResource CancelConverter}}" />
</Grid>
</ViewCell>
</DataTemplate>
Converter:
public class CancelConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Schedule schedule = ((Schedule)value);
DateTime date1 = schedule.ProposedCollectionDate;
DateTime date2 = schedule.OrderDate;
if (date1.CompareTo(date2) < 0)
{
return "Cancel this order";
}
}
}
The problem is that in the converter, value is null. How can I pass the object to the Converter, instead of just a single property of the object?

What you have shown looks good to me, but there may be something bad in what you've hidden. I can't tell, but are you properly setting the binding context of the ListView i.e. are you setting the ItemsSource property? From the information you've given, you should be setting it to something like an ObservableCollection<Schedule> or IEnumerable<Schedule> and then each item in the ListView would have a Schedule as its binding context and it would make its way into the value converter happily.

Related

Use ViewLocator inside TabControl.ContentTemplate

In an AvaloniaUI window, I want to have a TabControl whose tabs are added and removed from an ObservableCollection<T>. The tab's "title" (the text appearing on the tab strip) should be set inside each item of the collection, which could belong to a different type.
For that I defined a type:
public abstract class TabViewModelBase : ViewModelBase
{
public abstract string TabHeader { get; }
}
and my collection is defined like this:
public ObservableCollection<TabViewModelBase> OpenTabs { get; } = new();
In the axaml file, this is the definition of the TabControl:
<TabControl Items="{Binding OpenTabs}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding TabHeader}"/>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
So far, this works like a charm.
The problem begins when I also want to set up a container for the view inside each tab, which should not be a part of the contained view itself. I've tried by editing the xaml above and setting a ContentTemplate like this:
<TabControl Items="{Binding OpenTabs}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding TabHeader}"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<Border Child="{Binding}"/>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
However this results in the following error:
[Binding] Error in binding to 'Avalonia.Controls.Border'.'Child': 'Could not convert 'Project.ViewModels.TestingViewModel' to 'IControl'.'
This seems to be because ViewLocator, which automatically matches a view model to a view based on its name, is not being called. I assume this is because I've defined a DataTemplate inside TabControl.ContentTemplate.
Is it possible to instruct Avalonia to use ViewLocator inside TabControl.ContentTemplate, so that a view is selected based on its name?
<Border Child="{Binding}"/>
Border expects an actual control as a child, not a view model. You need to use ContentControl instead. It can also have it's own data template or view locator.
I found a way to work around the issue, by defining an IValueConverter that uses ViewLocator internally:
public class ViewModelValueConverter : IValueConverter
{
public object? Convert(
object value, Type targetType, object parameter, CultureInfo culture
)
{
if (value == null)
return null;
if (
value is ViewModelBase viewModel
&& targetType.IsAssignableFrom(typeof(IControl))
)
{
ViewLocator viewLocator = new();
return viewLocator.Build(value);
}
throw new NotSupportedException();
}
public object ConvertBack(
object value, Type targetType, object parameter, CultureInfo culture
)
{
throw new NotSupportedException();
}
}
and using it in XAML:
<Window.Resources>
<local:ViewModelValueConverter x:Key="variableView"/>
</Window.Resources>
<TabControl Items="{Binding OpenTabs}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding TabHeader}"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<Border Child="{Binding, Converter={StaticResource variableView}}"/>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
but it feels like there might be a simpler solution.

How can I make it so that a control is visible only if a binding value is not an empty space?

I have this XAML:
<t:FooterTemplate Text="{Binding SourceFooter }" />
The object has margins and color.
How can I make it so that it's not visible using the IsVisible, if the value of SourceFooter = "" ?
I want to do something like:
<t:FooterTemplate Text="{Binding SourceFooter }" IsVisible="{ SourceFooter != "" }" />
But I know that's not possible to do.
First Option: Converter
You can use a converter for this. For example:
<t:FooterTemplate Text="{Binding SourceFooter }" IsVisible="{Binding SourceFooter, Converter={StaticResource StringEmptyConverter} }" />
And then the converter looks like this:
public class StringEmptyConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return !String.IsNullOrWhiteSpace(((string)value));
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
The converter simply returns false if the string is whitespace or null.
To use it as a StaticResource, of course you'll also have to add a line in the ResourceDictionary in the App.xaml file.
<converters:StringEmptyConverter x:Key="StringEmptyConverter" />
Second option: DataTrigger
Triggers can modify the properties of the visual element. As the name says: they are triggered when a certain property (SourceFooter) equals a certain value ("").
<t:FooterTemplate Text="{Binding SourceFooter}" >
<t:FooterTemplate.Triggers>
<DataTrigger
Binding="{Binding SourceFooter}"
TargetType="t:FooterTemplate"
Value="">
<Setter Property="IsVisible" Value="false" />
</DataTrigger>
</t:FooterTemplate.Triggers>
</t:FooterTemplate>
What do I use?
In this case you should go for the Converter solution. You're probably gonna be checking if a string is empty somewhere else in your app.
In my opinion, DataTriggers can be useful for more exotic 'Triggers'.

How to number items in a Xamarin.Forms ListView using the index of each item in the ItemsSource, including when the items are removed?

I have a converter which takes in the ListView as a parameter to for each item as the List, and then, using the ItemsSource and IndexOf, I can determine the items position in the list and return that as a number for the View:
public class MyConverter : IValueConverter, IMarkupExtension
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var listView = parameter as ListView;
var collection = listView.ItemsSource as IList;
object item = value;
var answer = collection.Count - collection.IndexOf(item); //I'm actually numbering items in reverse.
return answer;
}
}
I bind to it using the entire binding context ( {Binding .} ) so that each item is passed as a value to it's own converter, like so:
<ListView x:Name="ListViewItself" ItemsSource="{Binding TheItemSource}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Label Text="{Binding ., Converter={converters:MyConverter}, ConverterParameter={x:Reference ListViewItself}}" />
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
This works very well. As I add items to my list, the converter correctly calculates the right item place.
My issue now, is that when I remove items from the collection and the ListView, the values for each remaining item in the ListView should automatically update, so as to take into account their new positions. But this is not the case. The converters are called, but with many null values for the items.
I want to know, how can I call RaisePropertyChanged for each binding context in the list so that the converter fires properly for all of them?
Edit:
I try to use
RaisePropertyChanged(nameof(TheItemSource)) when I remove an item from the collection, but it doesn't fire the converter for each item like I'd expect.

how to change gridview row color based on condition in uwp c#?

how to change gridview row color based on condition in uwp c#?
I want to highlight the gridview row based on my conditon.
A convenient way to do this would be to put a Border around your GridViewItem and use a ValueConverter to choose the background color based on the current item.
First you define your value converter:
public class ItemToColorConverter: IValueConverter
{
//this converts the item from your data source to the color brush
//of the background of the row
public object Convert(object value, Type targetType,
object parameter, string language)
{
//cast the value parameter to the type of item in your data source
var yourValue = ( YourType )value;
if ( yourValue > 10 ) //some condition you want to use to choose the color
{
//highlight
return new SolidColorBrush( Colors.Green );
}
else
{
//leave no background
return new SolidColorBrush( Colors.Transparent );
}
}
//you don't have to implement conversion back as this is just one-way binding
public object ConvertBack(object value, Type targetType,
object parameter, string language)
{
throw new NotImplementedException();
}
}
Now you need to create a Application resource instance of the converter in App.xaml:
<Application ...>
<Application.Resources>
<converters:ItemToColorConverter x:Key="ItemToColorConverter" />
</Application.Resources>
</Application>
Now use this converter in your GridView item DataTemplate:
<GridView ItemsSource="{Binding YourDataSource"}>
<GridView.ItemTemplate>
<DataTemplate>
<Border Background="{Binding Converter={StaticResource ItemToColorConverter}">
<!-- ... your content -->
</Border>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>

How to use StringFormat in XAML elements?

I'm deep in a XAML stack of elements binding to orders.
The order date displays as e.g. "12/31/2008 12:00:00 AM".
I want it to display as e.g. "31.12.2008".
How can I do this? I have seen other stackoverflow questions mention StringFormat but they use multibinding in ways that I can't get to work.
Here is the kind of syntax I would like (this is pseudocode), simply specifying StringFormat where you need it, is this possible somehow?
<StackPanel>
<ListView ItemsSource="{Binding Orders}">
<ListView.View>
<GridView>
<GridViewColumn
Header="Order ID"
DisplayMemberBinding="{Binding Path=OrderID}"
StringFormat="{}{1:dd.MM.yyyy}"/>
<GridViewColumn
Header="Order Date"
DisplayMemberBinding="{Binding Path=OrderDate}"/>
</GridView>
</ListView.View>
</ListView>
</StackPanel>
I haven't tested it, but I think this should work:
<GridViewColumn
Header="Order Date"
DisplayMemberBinding=
"{Binding Path=OrderDate, StringFormat='{}{0:dd.MM.yyyy}'}"/>
In general, you can look for an associated *StringFormat dependency property. For example, all ContentControl implementations (such as Label and Tooltip) have the ContentStringFormat dependency property:
<Label
Content="{Binding Path=DateAsked}"
ContentStringFormat="{}{0:yyyy/MM/dd HH:mm:ss}" />
In your case, while the GridViewColumn has the HeaderStringFormat dependency property to go along with Header, there is no analog for the DisplayMemberBinding and so you will need .NET 3.5 SP1 (get it with Visual Studio 2008 SP1) or better to use the new BindingBase.StringFormat Property:
<GridViewColumn
Header="Order ID"
DisplayMemberBinding="{Binding Path=OrderID, StringFormat='{}{0:dd.MM.yyyy}'}"
/>
There are lots more examples at the blog post Trying out Binding.StringFormat.
XAML
<UserControl.Resources>
<myNamespace:DateTimeConverter x:Key="DateTimeConverter" />
</UserControl.Resources>
<GridViewColumn
DisplayMemberBinding=="{Binding Path=OrderDate, Converter={StaticResource DateTimeConverter}}"
/>
C#
public class DateTimeConverter : IValueConverter
{
public object Convert(object value,
Type targetType,
object parameter,
CultureInfo culture)
{
if (value != null)
{
return ((DateTime)value).ToString("dd.MM.yyyy");
}
else
{
return String.Empty;
}
}
public object ConvertBack(object value,
Type targetType,
object parameter,
CultureInfo culture)
{
return DateTime.Parse(value.ToString());
}
}
If you wanted to localize the date format, you can include it in your .resx file. You will have to set up your app for localization by following this guide: https://developer.xamarin.com/guides/xamarin-forms/advanced/localization/.
The resx entry:
<data name="DateFormat" xml:space="preserve">
<value>{0:dddd d MMMM H:mm}</value>
</data>
In your content page, you then include the location of the resx file
xmlns:il8n="clr-namespace:MyProject.Localization;assembly=MyProject"
And then you can use it in your binding like so:
<Label
Text = "{Binding Time, StringFormat={il8n:Translate DateFormat}}"