I've got a TabControl that is bound to its viewmodel Items property:
<TabControl ItemsSource="{Binding Items}"/>
This is the viewmodel class:
public class ViewModel
{
public ObservableCollection<object> Items { get; } = new ObservableCollection<object>();
public ViewModel()
{
Items.Add(new Person() { FirstName = "Alan", LastName = "Turing" });
Items.Add(new Car() { ModelName = "Fiesta", Manifacturer = "Ford" });
}
}
Items's type is ObservableCollection<object>; this is because each tab represents a different kind of object, in this example a Person and a Car.
Question
I want to bind the tab's header to a text that is not available as a property of the bound classes, let say a manually provided header.
For example, I would like the first tab to have the text "Person" as header and the second to have the text "Car" as header:
I thought of:
adding the Header property to each class. It's easy and straightforward from the binding point of view, but this is sort of a generalized viewer and not all the interfaces of the input data can be modified.
an "attached property" would still require to act at class-level; yet feasible but probably a bit overkill
A Dictionary<object, string> defined at the viewmodel level, let's call it Headers.
Each time i add an object to the collection, i would also add an Header to the Headers dictionary. Its key would be the object itself.
Person p = new Person() { FirstName = "Alan", LastName = "Turing" };
Headers.Add(p, "Person");
Items.Add(p);
But how to write the binding expression then?
<TabControl ItemsSource="{Binding Items}">
<TabControl.ItemTemplate>
<DataTemplate>
<Label Content="{Binding ????????}"/>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
Even if possible, this solution seems to be not very 'binding-friendly'.
There seems to be two problems here: dynamically attach an information to an instance, and access it via binding.
Any ideas?
Postscript - the code
The code, for completeness.
My classes:
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class Car
{
public string ModelName { get; set; }
public string Manifacturer { get; set; }
}
My DataTemplate, defined in the <Winodow.Resources> tag.
<DataTemplate DataType="{x:Type local:Person}">
<StackPanel>
<Label Content="{Binding FirstName}"/>
<Label Content="{Binding LastName}"/>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type local:Car}">
<StackPanel>
<Label Content="{Binding Manifacturer}"/>
<Label Content="{Binding ModelName}"/>
</StackPanel>
</DataTemplate>
You can also override the ToString() method for your model classed and return your friendly name from there.
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public override string ToString()
{
return "Person";
}
}
Template
<TabControl ItemsSource="{Binding Items}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
Related
I'm using the example code from the ListView with Grouped Headers demo in the WinUI 3 Gallery. I've gotten it to sort the items in my list perfectly, but when it shows them the Group Header shows properly but there is no data shown for each of the objects in the list. It just shows "[ProjectName.ObjectClassName]", i.e. "TestApp.Contacts".
I can't figure out how to actually show the information that is in that Contacts group as it is shown in the demo.
Here is the example XAML:
<ListView ItemsSource="{x:Bind ContactsCVS.View, Mode=OneWay}">
<CollectionViewSource x:Name="ContactsCVS" IsSourceGrouped="True"/>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel AreStickyGroupHeadersEnabled="False"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.GroupStyle>
<GroupStyle >
<GroupStyle.HeaderTemplate>
<DataTemplate x:DataType="local1:GroupInfoList">
<Border AutomationProperties.Name="{x:Bind Key}">
<TextBlock Text="{x:Bind Key}" Style="{ThemeResource TitleTextBlockStyle}"/>
</Border>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
And the C#:
public static async Task<ObservableCollection<GroupInfoList>> GetContactsGroupedAsync()
{
// Grab Contact objects from pre-existing list (list is returned from function GetContactsAsync())
var query = from item in await GetContactsAsync()
// Group the items returned from the query, sort and select the ones you want to keep
group item by item.LastName.Substring(0, 1).ToUpper() into g
orderby g.Key
// GroupInfoList is a simple custom class that has an IEnumerable type attribute, and
// a key attribute. The IGrouping-typed variable g now holds the Contact objects,
// and these objects will be used to create a new GroupInfoList object.
select new GroupInfoList(g) { Key = g.Key };
return new ObservableCollection<GroupInfoList>(query);
}
// GroupInfoList class definition:
public class GroupInfoList : List<object>
{
public GroupInfoList(IEnumerable<object> items) : base(items)
{
}
public object Key { get; set; }
}
// Contact class definition:
public class Contact
{
public string FirstName { get; private set; }
public string LastName { get; private set; }
public string Company { get; private set; }
public string Name => FirstName + " " + LastName;
public Contact(string firstName, string lastName, string company)
{
FirstName = firstName;
LastName = lastName;
Company = company;
}
// ... Methods ...
}
ContactsCVS.Source = await Contact.GetContactsGroupedAsync();
And this is what it should look like:
I was able to figure it out. In between ListView.ItemsPanel and ListView.GroupStyle I need to add the following section:
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:Contact">
<Border AutomationProperties.Name="{x:Bind Name}">
<TextBlock Text="{x:Bind Name}" Style="{ThemeResource TitleTextBlockStyle}"/>
</Border>
</DataTemplate>
</ListView.ItemTemplate>
That inserted the Contact.Name property instead of the name of the object. Microsoft doesn't include this in their documentation, as far as I can tell.
I am currently attempting to mock up data for my view by utilizing Design Data for my ViewModel. Specifically, I have a View front end, and a ViewModel backend for my silverlight application.
When I have mocked up other views, things have worked perfectly. Even in this particular view, the only issue seems to have to be with collections.
Any idea why my "CategoryItem" keeps giving me an error when I try to assign a value to "CategoryName"? I have no idea what is causing the issue...
Code below:
My Design Data:
<vm:MainPageViewModel
xmlns:vm="clr-namespace:WebCatalog.ViewModels"
xmlns:m="clr-namespace:WebCatalog.Models"
SelectedTab="Category 1"
ProjectName="New Project Name"
ShowPopup="False"
IsBusy="False"
CurrentUser="Alex"
>
<vm:MainPageViewModel.Categories>
<m:CategoryItem CategoryName="test"/>
</vm:MainPageViewModel.Categories>
</vm:MainPageViewModel>
My simplified ViewModel:
public class MainPageViewModel {
public string SelectedTab {get;set;}
public string ProjectName {get;set;}
public bool ShowPopup {get;set;}
public bool IsBusy {get;set;}
public string CurrentUser {get;set;}
public ObservableCollection<CategoryItem> Categories {get;private set;}
public MainPageViewModel()
{
Categories = new ObservableCollection<CategoryItem>();
}
}
Finally, my (simplified) view:
<UserControl
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
d:DataContext="{d:DesignData Source=../SampleData/MainWindowSampleData.xaml}">
<!-- Decision Categories -->
<StackPanel Width="200" toolkit:DockPanel.Dock="Left" Height="100">
<TextBlock Text="{Binding CurrentUser}">meep</TextBlock>
<ItemsControl Height="100" ItemsSource="{Binding Categories}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock>Itemalkdjfa;ldfj;lakdsjfladfjal;dfjaldfja</TextBlock>
<TextBlock Text="{Binding CategoryName}"></TextBlock>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
On your VM change
public ObservableCollection<CategoryItem> Categories {get;private set;}
to
public ObservableCollection<CategoryItem> Categories {get;set;}
Otherwise my sample xaml binding looked and worked in design mode like this:
<local:MainVM x:Key="myMainVM">
<local:MainVM.Categories>
<local:CategoryItem Name="Test" />
</local:MainVM.Categories>
</local:MainVM>
...
<ListBox DataContext="{StaticResource myMainVM}"
ItemsSource="{Binding Categories}">
<ItemsControl.ItemTemplate>
<DataTemplate><TextBlock Text="{Binding Name}"/></DataTemplate>
</ItemsControl.ItemTemplate>
</ListBox>
Where the VM mirrored yours (sans the private set) and the VM was loaded in Xaml on Silverlight and showed the data in design mode.
public class MainVM
{
public ObservableCollection<CategoryItem> Categories { get; set; }
public MainVM()
{
Categories = new ObservableCollection<CategoryItem>();
}
}
public class CategoryItem
{
public string Name { get; set; }
}
I am new to WPF and wondering, how I could bind multiple and different Controls to a ItemControl in a C# WPF Application?
With my current code I can just put labels and textboxes into my ItemControl. Because of the template definition in my XAML.
But I like to be flexible to put labels and other Controls like combobox, datepicker, etc. into my ItemControl...
How could I do this?
I attached my current XAML Code with a Data Template and the code behind. And here a small picture of the current look and my target what I want:
XAML-CODE:
<Window x:Class="WPFFormTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="400">
<Grid>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl x:Name="icFields">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid x:Name="FieldTypes" Margin="10,10,10,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="75"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Label Content="{Binding FieldName}" />
<TextBox Grid.Column="1" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
C#-Code:
namespace WPFFormTest
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
List<Fields> fields = new List<Fields>();
fields.Add(new Fields() { FieldName = "test1", FieldValue = "1" });
fields.Add(new Fields() { FieldName = "test2", FieldValue = "2" });
fields.Add(new Fields() { FieldName = "test3", FieldValue = "3" });
icFields.ItemsSource = fields;
}
}
public class Fields
{
public string FieldName { get; set; }
public string FieldValue { get; set; }
public string FieldType { get; set; }
}
}
One Approach could be: add all needed controls to the template and switch the visibility. But some help from the view model is needed:
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ScrollViewer.Resources>
<BooleanToVisibilityConverter x:Key="BoolConverter"/>
</ScrollViewer.Resources>
<ItemsControl x:Name="icFields">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid x:Name="FieldTypes" Margin="10,10,10,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="75"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Label Content="{Binding FieldName}" />
<TextBox Grid.Column="1" Visibility="{Binding ShowText, Converter={StaticResource BoolConverter}}"/>
<CheckBox Grid.Column="1" Visibility="{Binding ShowCheckBox, Converter={StaticResource BoolConverter}}" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
public class Fields
{
public string FieldName
{
get;
set;
}
public string FieldValue
{
get;
set;
}
public bool ShowText
{
get;
set;
}
public bool ShowCheckBox
{
get;
set;
}
private int fieldType;
public int FieldType
{
get
{
return fieldType;
}
set
{
fieldType = value;
ShowText = false;
ShowCheckBox = false;
switch (fieldType)
{
case 0:
ShowText = true;
break;
case 1:
ShowCheckBox = true;
break;
}
}
}
}
You might want to consider using an MVVM framework. I know that Caliburn.Micro has some built-in features to make this sort of thing easy, using actual OO features like polymorphism.
First you would define a separate viewmodel class for each item type:
public class FieldViewModel { }
public class TextViewModel : FieldViewModel { public string Value {get;set;} }
public class DateViewModel : FieldViewModel { public DateTime Value {get;set;} }
Then you can define a UserControl called TextView and another called DateView. When you bind your list of FieldViewModel to the UI, Caliburn.Micro will automatically create the right type of view and bind its properties.
It takes a little bit of work to get Caliburn.Micro up and running (you need to hook up the bootstrapper and create a shell viewmodel, at the very least) so I won't provide a complete code sample here -- I'm just trying to give you a flavor for whether it would solve your problem. For the actual code you need, work your way through their documentation; they do a good job of walking you through everything.
The simplest way to do that is to just provide different DataTemplates for different data types. For example, if you had a Person class, you could define a DataTemplate with Name, Age, and Sex TextBox fields, whereas if you had a Car class, you could define a DataTemplate with Make, Model and Year ComboBox fields:
<DataTemplate DataType="{x:Type DataTypes:Person}">
<!--Define TextBoxes here-->
</DataTemplate>
<DataTemplate DataType="{x:Type DataTypes:Car}">
<!--Define ComboBoxes here-->
</DataTemplate>
By defining these DataTemplates without specifying the x:Key reference values, this means that they will be automatically applied by the Framework, whenever it comes across any instances of these types. Therefore, this will display Person details in TextBoxes:
<ItemsControl ItemsSource="{Binding Persons}" ... />
And this will display Car object details in ComboBoxes:
<ItemsControl ItemsSource="{Binding Cars}" ... />
PersonVM.cs
public class MainWindowVM
{
public MainWindowVM()
{
PersonList = new ObservableCollection<Person>(Employees);
}
private Person[] Employees = new Person[]
{
new Person { ID = 1, Name = "Adam" },
new Person { ID = 2, Name = "Bill" },
new Person { ID = 10, Name = "Charlie" },
new Person { ID = 15, Name = "Donna" },
new Person { ID = 20, Name = "Edward" }
};
public ObservableCollection<Person> PersonList { get; set; }
}
Person.cs
public class Person
{
public string Name { get; set; }
public int ID { get; set; }
}
MainWindow.xaml (Functionally working version -- Not what I want to display)
<Window x:Class="TestApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<ComboBox Height="23" Width="300"
ItemsSource="{Binding Path=Objects}"
DisplayMemberPath="Name"
>
</ComboBox>
</Grid>
</Window>
MainWindow.xaml (Displays correctly -- Doesn't function properly)
<Window x:Class="TestApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<ComboBox Height="23" Width="300"
ItemsSource="{Binding Path=Objects}"
>
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock DataContext="{Binding}">
<TextBlock.Text>
<MultiBinding StringFormat="{} {0} | {1}">
<Binding Path="ID" />
<Binding Path="Name" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</Window>
The second code displays what I want the ComboBox to display {ID} | {Name}, but it takes away a common function of the ComboBox. In the first example, when the ComboBox is selected the user can start typing into it and have it jump down the list. For example, if you press the letter A it jumps to "Adam", B jumps to "Bill", etc. This is how a ComboBox is supposed to function. But, when I override the ComboBox ItemTemplate it loses that functionality. Is there another way to Bind what I need and keep that functionality or to reenable it? Perhaps the ItemTemplate is setup wrong?
See my answer to this question: Can I do Text search with multibinding
Unfortunately TextSearch.Text doesn't work in a DataTemplate. I think you have two options here
Option 1. Set IsTextSearchEnabled to True for the ComboBox, override ToString in your source class and change the MultiBinding in the TextBlock to a Binding
<ComboBox ...
IsTextSearchEnabled="True">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
public class Person
{
public override string ToString()
{
return String.Format("{0} | {1}", Name, ID);
}
public string Name { get; set; }
public int ID { get; set; }
}
Option 2. Make a new Property in your source class where you combine Name and ID for the TextSearch.TextPath. Also, you should call OnPropertyChanged for NameAndId whenever you do it for Name or ID
<ComboBox ...
TextSearch.TextPath="NameAndId"
IsTextSearchEnabled="True">
public string NameAndId
{
return String.Format("{0} | {1}", Name, ID);
}
I have a class, for experiment sake call it foo() and another class, call it bar()
I have a data template for class foo() defined in my xaml, but one of foo()'s properties is a bar() object such that
foo()
{
Public string Name {get; set;}
Public int ID {get; set;}
Public bar barProp {get; set;}
}
and
bar()
{
Public string Description{get; set;}
}
I want my data template of foo to display the Description property of bar.
I have tried the simple <textblock Text="{Binding Path=barProp.Description}" /> and variants to no avail
Seeking wisdom,
DJ
EDIT:
In accordance with requests for more information...
here are my real classes...
public class AccountRecord
{
public string Value { get; set; }
public string Identifier { get; set; }
public Field AccountNumber;
}
public class Field
{
public string Name{get;set;}
public string Value{get;set}
}
and here is the XAML used to template them them...
<ListBox Margin="0,35,0,0" Name="listAccountRecords" Background="Transparent" BorderBrush="Transparent" ItemsSource="{Binding AccountRecords, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type lib:AccountRecord}">
<Expander Header="{Binding AccountNumber.Name}">
<ListView ItemsSource="{Binding Fields}" ItemTemplate="{DynamicResource FieldTemplate}">
</ListView>
</Expander>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
And the exact problem is that the AccountNumber.Name value is not showing up in the expander header for the AccountRecord element
Your "AccountNumber" member (of type "Field") is only a field, not a property. You can only bind to properties. Give it a getter and setter and it'll start working.
Try This
<textblock Text="{Binding Path=FooObjectName.barProp.Description}" />
Hope this will work..
Good Luck !