I am using C#, WPF, .NET Standard, Visual Studio. All the latest or almost latest versions.
This is my datacontext model (which is created in seperated library called ProgrammingManagerAPI):
public class MainModel
{
public List<Project> Projects { get; set; }
...
}
which have list of object of type Project defined like this (also in seperated library ProgrammingManagerAPI, in directory Models), some properties and some methods:
public class Project
{
public int Id { get; set; }
...
public TimeSpan? TotalWorkedTime(bool subtasksIncluded = true)
{
if (Id < 0)
return null;
else
return new TimeSpan(...);
}
...
}
In mainWindow I have a ListView, which I want to use to list projects with its properties.
I have lots of properties and some methods which are giving back the value depending on boolean parameter.
I read that in this case I should use ObjectDataProvider, so I tried like below:
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:API.Models="clr-namespace:ProgrammingManagerAPI.Models;assembly=ProgrammingManagerAPI"
<Window.Resources>
<ObjectDataProvider x:Key="yourStaticData"
ObjectType="{x:Type API.Models:Project}"
MethodName="TotalWorkedTime" >
<ObjectDataProvider.MethodParameters>
<s:Boolean>false</s:Boolean>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</Window.Resources>
<Grid Grid.Row="1" Grid.Column="0" Margin="10">
<ListView Margin="10" ItemsSource="{Binding Projects}" HorizontalAlignment="Center" HorizontalContentAlignment="Center">
<ListView.View>
<GridView>
<GridViewColumn HeaderContainerStyle="{StaticResource ListViewStyle}" Header="Id" DisplayMemberBinding="{Binding Id}" />
<GridViewColumn HeaderContainerStyle="{StaticResource ListViewStyle}" Header="TotalWorkedTime" DisplayMemberBinding="{Binding Path=., Source={StaticResource yourStaticData}}" />
</GridView>
</ListView.View>
</ListView>
</Grid>
The call to the function TotalWorkedTime is fired, because breakpoint is hit. But is hit once, while I have created 4 object for test. Moreover it is hit like static function, not for every instance of the object like other properties. In immediate window I am trying to see what are other properties and those are nulls. While the column for Id is hit all the properties are available for each instance of Project. Moreover I have observed that it is hit before Id property getter is called.
I have tried many versions like without Path, in Binding and so many others ways.
Anyone can point me my mistake?
ObjectDataProvider
is useful when you have one single instance of an object (or a static class) you want to bind to, but you're using an ItemsControl
(ListView
), which makes things a bit more complicated.
What you need is an IValueConverter
. That takes an object and "converts" it by calling a function and returning the result. I honestly expected to be able to find one by Googling, but I wasn't able to. I thought I might end up using something like this sooner or later so I went ahead and built one. This supports any type of object with any function name taking any number of parameters.
public class FunctionConverter : IValueConverter
{
public string FunctionName { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value.GetType().GetMethod(FunctionName).Invoke(value, (object[])parameter);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return Binding.DoNothing;
}
}
And here's an example of how you might use it:
MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
TestInstance = new Test();
InitializeComponent();
}
public Test TestInstance { get; set; }
}
public class Test
{
public string Foo(string bar)
{
return bar;
}
}
MainWindow.xaml:
<Grid>
<Grid.Resources>
<KHS:FunctionConverter x:Key="FuncCon" FunctionName="Foo"/>
</Grid.Resources>
<TextBlock>
<TextBlock.Text>
<Binding Path="TestInstance" Converter="{StaticResource FuncCon}">
<Binding.ConverterParameter>
<x:Array Type="sys:Object">
<sys:String>Hello World</sys:String>
</x:Array>
</Binding.ConverterParameter>
</Binding>
</TextBlock.Text>
</TextBlock>
</Grid>
You declare the converter as a resource, just like you did with the ObjectDataProvider
, and set FunctionName
to the name of the function you want to call. The converter then uses MethodInfo.Invoke(Object, Object[])
to run that function and returns the result.
You pass parameters for the function via the binding's ConverterParameter
property, which would let you potentially pass different values for different items in your list. In the example, I pass the string "Hello World"
to the function Foo
, which just returns exactly what was passed.
A few final notes: This converter only works one way. The converter as provided doesn't check for null
and has no handling in place for when FunctionName
is not found. Using a binding like this doesn't allow for update notifications like a dependency property would provide.