Embed DataGrid into WPF Treeview nodes - wpf

I need to display Hierarchy in the treeview. But Details should be displayed in the datagrid.
How do I have to write my template to achieve this? I misunderstand smth in templates for now.
<TreeView ItemsSource="{Binding Path=Categories}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type stackProjects:Category}" ItemsSource="{Binding Path=SubCategories}">
<TextBlock Margin="3" Text="{Binding Path=CategoryName}"/>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type stackProjects:SubCategory}" ItemsSource="{Binding Path=Details}">
<TextBlock Text="{Binding Path=SubCategoryName}"/>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type stackProjects:Detail}" >
<StackPanel Orientation="Horizontal">
<TextBlock Margin="3" Text="{Binding Path=Name}"/>
<TextBlock Margin="3" Text=" - "/>
<TextBlock Margin="3" Text="{Binding Path=Info}"/>
</StackPanel>
</DataTemplate>
</TreeView.Resources>
</TreeView>

I've found a workaround. I had to understand that Details should be presented as a collection within a single element which has IEnumerable property. May be it's not the best solution but it works.
I needed to create a Converter to wrap my collection into single one.
public class BoxingItemsConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var values = value as IEnumerable;
var type = parameter as Type;
if (values == null || type == null)
return null;
if (type.GetInterfaces().Any(x => x == typeof (IItemsWrapper)))
{
var instance = (IItemsWrapper) type.Assembly.CreateInstance(type.FullName);
instance.Items = (IEnumerable) value;
//returned value should be IEnumerable with one element.
//Otherwise we will not see children nodes
return new List<IItemsWrapper> {instance};
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Example for wrappers:
internal interface IItemsWrapper
{
IEnumerable Items { get; set; }
}
public class ItemsWrapper : IItemsWrapper
{
public IEnumerable Items { get; set; }
}
public class DetailItemsWrapper : ItemsWrapper{}
public class InvoiceItemsWrapper:ItemsWrapper{}
And the xaml. It will not require a lot of changes. You just need to use Boxing converter and set the Type to return in the converter parameter.
<TreeView ItemsSource="{Binding Path=Categories}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type wpfProj:Category}" ItemsSource="{Binding Path=SubCategories}">
<TextBlock Margin="4" Text="{Binding Path=CategoryName}"/>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type wpfProj:SubCategory}"
ItemsSource="{Binding Path=Details, Converter={StaticResource boxingItems}, ConverterParameter={x:Type wpfProj:DetailItemsWrapper}}" >
<StackPanel>
<TextBlock Margin="4" Text="{Binding Path=SubCategoryName}"/>
</StackPanel>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type wpfProj:DetailItemsWrapper}" >
<DataGrid ItemsSource="{Binding Path=Items}"/>
</DataTemplate>
</TreeView.Resources>
</TreeView>
I've uploaded sample to dropbox.
Here is how it does look like:

Related

WPFLocalizationExtension with ItemTemplate

I'm using WPFLocalizationExtension for my WPF app.
I have one ComboBox for languages selection. Item source is an ObservableCollection<KeyValuePair<string, string>> as below:
TITLE_LANGUAGE_ENGLISH : en
TITLE_LANGUAGE_VIETNAMESE: vi-VN
This is my xaml code:
<TextBlock Text="{lex:Loc TITLE_LANGUAGE}"></TextBlock>
<ComboBox Grid.Column="1"
ItemsSource="{Binding AvailableLanguages}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{lex:Loc Key={Binding Key}}"></TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
When I run the application, it throws me an exeption as below:
A 'Binding' cannot be set on the 'Key' property of type 'LocExtension'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject
How can I translate the ItemTemplate ?
Thank you,
You could use an IMultiValueConverter together with a MultiBinding, so that you don't loose the ability to update the localization on-the-fly.
<ComboBox ItemsSource="{Binding AvailableLanguages}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock>
<TextBlock.Text>
<MultiBinding>
<MultiBinding.Bindings>
<Binding Path="Key" Mode="OneTime"/>
<Binding Path="Culture" Source="{x:Static lex:LocalizeDictionary.Instance}"/>
</MultiBinding.Bindings>
<MultiBinding.Converter>
<l:TranslateMultiConverter/>
</MultiBinding.Converter>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
And here is the converter:
class TranslateMultiConverter : DependencyObject, IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length == 2)
{
var key = values[0] as string;
if (key == null)
{
return DependencyProperty.UnsetValue;
}
var cultureInfo = (values[1] as CultureInfo) ?? culture;
return LocalizeDictionary.Instance.GetLocalizedObject(key, this, cultureInfo);
}
return values.FirstOrDefault();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
The LocalizeDictionary will raise a PropertyChanged event when the app's language will change causing the MultiBinding to refresh the values.
Note that the converter is a DependencyObject too. This is to provide the context to the LocalizeDictionary when calling the GetLocalizedObject method.
you have to bind to the Path Key directly. The TextBlock at DataTemplate points directly to a single KeyValuePair object, that you can access the property Key directly.
<TextBlock Text="{lex:Loc TITLE_LANGUAGE}"></TextBlock>
<ComboBox Grid.Column="1"
ItemsSource="{Binding AvailableLanguages}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Key}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Update:
Maybe you have to add a Converter. Try WPFLocalizeExtension.TypeConverters.DefaultConverter or implement a class deriving from IValueConverter by yourself.
<ComboBox.Resources>
<cv:DefaultConverter x:Key="DConv" />
</ComboBox.Resources>
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Key, Converter={StaticResource DConv}}" />
</DataTemplate>
</ComboBox.ItemTemplate>

WPF binding TextBox and ObservableDictionary<Int64,String> (display String by Id)

Each item of my Employee's list has Post property. This property is Int64 type. Also, I have some ObservableDictionary<Int64,String> as static property. Each Employe must display the String value by its key.
DataTemplate for Employe item (I deleted the superfluous):
<DataTemplate x:Key="tmpEmploye">
<Border BorderThickness="3" BorderBrush="Gray" CornerRadius="5">
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Path=Post}"/>
</StackPanel>
</Border>
</DataTemplate>
But this code displayed the Int64 value, not the String. String for getting static dictionary:
"{Binding Source={x:Static app:Program.Data}, Path=Posts}"
I know how solve it problem for ComboBox, but I don't know for TextBlock. For ComboBox I wrote it (it is works fine):
<ComboBox x:Name="cboPost" x:FieldModifier="public" Grid.Row="4" Grid.Column="1" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" Margin="2" Grid.ColumnSpan="2"
ItemsSource="{Binding Source={x:Static app:Program.Data}, Path=Posts}" DisplayMemberPath="Value"
SelectedValuePath="Key"
SelectedValue="{Binding Path=Post, Mode=TwoWay}">
</ComboBox>
But how can I solve it for TextBlock?
mmmmm, I'm sure I have developed something for this scenario before but I can't remember or find anything related!
IMO you can use a converter, so you pass your Post (Int64) to the converter and it returns the string value from the dictionary, although it must be a better solution.
[ValueConversion(typeof(Int64), typeof(string))]
public class PostToStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
// validation code, etc
return (from p in YourStaticDictionary where p.Key == Convert.ToInt64(value) select p.Value).FirstOrDefault();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
}
}
XAML:
<Window ...
xmlns:l="clr-namespace:YourConverterNamespace"
...>
<Window.Resources>
<l:PostToStringConverter x:Key="converter" />
</Window.Resources>
<Grid>
<TextBlock Text="{Binding Post, Converter={StaticResource converter}}" />
</Grid>
</Window>

How do I Bind 2 controls to 1 field AND also access the 2 control values for ConvertBack?

In a DataGridTemplateColumn DataTemplate,
I want to bind 2 controls to a string field of format "[name]:[value]" i.e. the string is delimited by colon ":". I need to bind control a) to the [name] part and control b) the value part.
I have been able to successfully use an IValueConverter to split the string for display:
public class NameAndValueConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string rtn = "";
string[] split = value.ToString().Split(':');
if (split.Count() == 2)
{
if(parameter.ToString() == "Name")
rtn = split[0];
if(parameter.ToString() == "Value")
rtn = split[1];
}
return rtn;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new InvalidOperationException("NameAndValueConverter can only be used OneWay.");
}
}
And the XAML:
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<StackPanel.Resources>
<local:NameAndValueConverter x:Key="NameAndValueConverter" />
</StackPanel.Resources>
<TextBox x:Name="namePart" Text="{Binding Path=FieldType, Converter={StaticResource NameAndValueConverter}, ConverterParameter='Name'}" />
<TextBox x:Name="valuePart" Text="{Binding Path=FieldType, Converter={StaticResource NameAndValueConverter}, ConverterParameter='Value'}" />
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
But the data may be edited in the Textboxes so how can I access the 2 TextBox values in ConvertBack so that they can be joined again?
Doing this in the XAML:
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<StackPanel.Resources>
<local:NameAndValueConverter x:Key="NameAndValueConverter" />
</StackPanel.Resources>
<TextBox x:Name="namePart" Text="{Binding Path=FieldType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, Converter={StaticResource NameAndValueConverter}, ConverterParameter='Name'}" />
<TextBox x:Name="valuePart" Text="{Binding Path=FieldType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, Converter={StaticResource NameAndValueConverter}, ConverterParameter='Value'}" />
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
should be enough. You need a TwoWay binding to edit from UI, and when you set UpdateSourceTrigger to PropertyChanged the TextBox will update itself automatically when the property will be modified in the ViewModel (you'd obviously need to implement INotifyPropertyChanged )

WPF pass parent binding object to the converter

I have ItemsControl that is bound to collection of type Student.
Inside the ItemTemplate I have a TextBox that uses IValueConverter to do some custom calculations and logic. I want to pass the actual Student object to the value converter, instead a property of it. How can I do that? Here's a sample of my code.
<ItemsControl ItemsSource="{Binding StudentList}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding ????, Converter={StaticResource MyConverter}}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
In the code I have this
public class MyValueConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
// I want 'value' to be of type Student.
return null;
}
}
You can just leave out the path. That way you get at the actual object bound to.
<TextBlock Text="{Binding Converter={StaticResource MyConverter}}"/>
or if you want to be explicit about it:
<TextBlock Text="{Binding Path=., Converter={StaticResource MyConverter}}"/>

Bind listbox in WPF with grouping

I've got a collection of ViewModels and want to bind a ListBox to them. Doing some investigation I found this.
So my ViewModel look like this (Pseudo Code)
interface IItemViewModel
{
string DisplayName { get; }
}
class AViewModel : IItemViewModel
{
string DisplayName { return "foo"; }
}
class BViewModel : IItemViewModel
{
string DisplayName { return "foo"; }
}
class ParentViewModel
{
IEnumerable<IItemViewModel> Items
{
get
{
return new IItemViewModel[] {
new AItemViewModel(),
new BItemViewModel()
}
}
}
}
class GroupViewModel
{
static readonly GroupViewModel GroupA = new GroupViewModel(0);
static readonly GroupViewModel GroupB = new GroupViewModel(1);
int GroupIndex;
GroupViewModel(int groupIndex)
{
this.GroupIndex = groupIndex;
}
string DisplayName
{
get { return "This is group " + GroupIndex.ToString(); }
}
}
class ItemGroupTypeConverter : IValueConverter
{
object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is AItemViewModel)
return GroupViewModel.GroupA;
else
return GroupViewModel.GroupB;
}
}
And this XAML
<UserControl.Resources>
<vm:ItemsGroupTypeConverter x:Key="ItemsGroupTypeConverter "/>
<CollectionViewSource x:Key="GroupedItems" Source="{Binding Items}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription Converter="{StaticResource ItemsGroupTypeConverter }"/>
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</UserControl.Resources>
<ListBox ItemsSource="{Binding Source={StaticResource GroupedItems}}">
<ListBox.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayName}" FontWeight="bold" />
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListBox.GroupStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayName}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
This works somehow, exept of the fact that the binding of the HeaderTemplate does not work. Anyhow I'd prefer omitting the TypeConverter and the CollectionViewSource. Isn't there a way to use a property of the ViewModel for Grouping?
I know that in this sample scenario it would be easy to replace the GroupViewModel with a string an have it working, but that's not an option. So how can I bind the HeaderTemplate to the GroupViewModel?
Ok, I finally solved it myself.
First of all the TypeConverter is unnecessary, because the PropertyGroupDescription can be bound on a PropertyName. So I added a Property 'Group' to IItemViewModel and modified XAML as follows:
<UserControl.Resources>
<CollectionViewSource x:Key="GroupedItems" Source="{Binding Items}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Group"/>
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</UserControl.Resources>
Furthermore the HeaderTemplate's DataContext is a CollectionViewGroupInternal and the binding has to look like this:
<DataTemplate>
<TextBlock Text="{Binding Name.DisplayName}" FontWeight="bold" />
</DataTemplate>
Here CollectionViewGroupInternal.Name resolves the actual GroupViewModel.

Resources