I am having issues using ListView Grouping to get the content of my lists to display in labels. The ListView's ItemsSource and GroupDisplayBinding are setting properly, but the label inside the ListView will not display anything (I even tried setting it to a literal string). All of my lists, as well as my "list of lists" are populating correctly. I have poured over the documentation, website articles, and instructional videos but I am still stuck. Can someone point me in the right direction? I am still having a tough time with databinding and MVVM in general. Thanks.
View:
<ListView ItemsSource="{Binding OAllLists}"
GroupDisplayBinding="{Binding Type}"
IsGroupingEnabled="true"
HasUnevenRows="True">
<ListView.ItemTemplate>
<DataTemplate>
<Label Text="{Binding Items}">
</Label>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
ViewModel:
class OrganizedViewModel
{
public ObservableCollection<Categories> OAllLists { get; set; }
public OrganizedViewModel()
{
OAllLists = new ObservableCollection<Categories>();
foreach(Categories value in Categories.AllLists)
{
OAllLists.Add(value);
}
}
}
Model:
public class Categories
{
public static List<Categories> AllLists { get; set; } = new List<Categories>();
public static Categories FruitList { get; set; } = new Categories("Fruit");
public static Categories VegetableList { get; set; } = new Categories("Vegetables");
///Each type of item has its own static Categories object
public string Type { get; set; }
public List<string> Items { get; set; } = new List<string>();
}
Method for organizing items:
class OrganizeIt
{
public void OrganizeItems(List<string> groceries)
{
foreach (string value in groceries) ///Checks each value for keywords
{
if (Keywords.fruitKeys.Any(value.Contains))
{
Categories.FruitList.Items.Add(value);
}
else if (Keywords.vegetableKeys.Any(value.Contains))
{
Categories.VegetableList.Items.Add(value);
}
}
///Adds each type of list to "list of lists" if it contains values
if (Categories.FruitList.Items.Any())
{
Categories.AllLists.Add(FruitItem);
}
if (Categories.VegetableList.Items.Any())
{
Categories.AllLists.Add(Categories.VegetableList);
}
Edit
New class per comment's recommendation. I also created another Observable Collection in the ViewModel populated by GroupedList (both are working correctly). Name changes are for clarity.
public class Groceries : List<string>
{
public string Category { get; set; }
public static List<Groceries> GroupedList { get; set; } = new List<Groceries>();
public static Groceries Fruits { get; set; } = new Groceries("Fruit");
public static Groceries Vegetables { get; set; } = new Groceries("Vegetables");
public Groceries(string s)
{
Category = s;
}
}
New View:
<ListView ItemsSource="{Binding OGroupedList}"
GroupDisplayBinding="{Binding Category}"
IsGroupingEnabled="true">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Label Text="{Binding .}"
VerticalOptions="FillAndExpand"/>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Edit 2
This is how I'm populating the ObservableCollection in my ViewModel now:
class OrganizedViewModel
{
public ObservableCollection<Groceries> OGroupedList { get; set; }
public string Category { get; set; }
public OrganizedViewModel()
{
OGroupedList = new ObservableCollection<Groceries>();
foreach (Groceries value in Groceries.GroupedList)
{
OGroupedList.Add(value);
}
}
Edit 3
This is the method for organizing items. It takes in a string list and checks each list item to see if it contains any of the keywords associated with a certain category (ex. "apple" is contained in "2 bags of apples"). If so, the list item is added to the corresponding Groceries object.
class OrganizeIt
{
public void OrganizeItems(List<string> groceries)
{
foreach (string value in groceries)
{
if (Keywords.fruitKeys.Any(value.Contains))
{
Groceries.Fruits.Add(value);
}
else if (Keywords.vegetableKeys.Any(value.Contains))
{
Groceries.Vegetables.Add(value);
}
}
if (Groceries.Fruits.Any())
{
Groceries.GroupedList.Add(Groceries.Fruits);
}
if (Groceries.Vegetables.Any())
{
Groceries.GroupedList.Add(Groceries.Vegetables);
}
}
Here is where the method is called on the MainPage. UnorganizedList is populated from user input.
private void SortItems()
{
OrganizeIt o = new OrganizeIt();
o.OrganizeItems(UnorganizedList);
}
Solution
All that was needed was to change the Label's binding to just {Binding} (with no period), as well as remove the "x:DataType" line. Below in the revised View in case this helps anybody:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:GroceryListMobile.ViewModels"
xmlns:mvvm="clr-namespace:MvvmHelpers;assembly=MvvmHelpers"
xmlns:model="clr-namespace:GroceryListMobile.Models"
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
x:Class="GroceryListMobile.Views.OrganizedView"
x:Name="Organized">
<ContentPage.BindingContext>
<viewmodels:OrganizedViewModel/>
</ContentPage.BindingContext>
<ContentPage.Content>
<ListView ItemsSource="{Binding OGroupedList}"
GroupDisplayBinding="{Binding Category}"
IsGroupingEnabled="true">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Label Text="{Binding}"
VerticalOptions="FillAndExpand"/>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentPage.Content>
</ContentPage>
You need a <ViewCell> as the child node of your <DataTemplate>:
<ListView ItemsSource="{Binding OAllLists}"
GroupDisplayBinding="{Binding Type}"
IsGroupingEnabled="true"
HasUnevenRows="True">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Label Text="{Binding .}">
</Label>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
ListView.ItemTemplate always requires a DataTemplate which is a Cell.
EDIT
The next thing to address is the collection used as the ItemSource. When using grouping, that needs to be a collection of collections. The thing I would try is to change your model class:
public class Categories : List<string>
{
public static List<Categories> AllLists { get; set; } = new List<Categories>();
public static Categories FruitList { get; set; } = new Categories("Fruit");
public static Categories VegetableList { get; set; } = new Categories("Vegetables");
///Each type of item has its own static Categories object
public string Type { get; set; }
}
And see the revised Binding in the Xaml above.
A way to think about grouped ListViews is that it is a list of lists. The "outer" list elements have a member bound to GroupDisplayBinding, and the "inner" list elements have members bound to the elements in the DataTemplate.
In your case, the "outer" collection is the ObservableCollection, so the GroupDisplayBinding will bind to something on Categories. Then each Categories needs to be a collection itself. In the revisions, that is a List, so the DataTemplate is given a string as the BindingContext. So the Label just needs to bind to the string (hence the Binding ..
The rest of the code would have to adjust a bit as the Items collection that used to be a member of Categories is now the Categories object itself (through inheritance).
EDIT 2
I created a new app with a page containing the ListView exactly as you have it (New View under your first Edit), and the Groceries class exactly as it is under the first Edit.
I revised OrganizedViewModel a little. It doesn't seem to use Category, and I wanted to make sure OGroupedList was being populated with Categories:
class OrganizedViewModel
{
public ObservableCollection<Groceries> OGroupedList { get; set; }
public OrganizedViewModel()
{
OGroupedList = new ObservableCollection<Groceries>();
OGroupedList.Add(Groceries.Fruits);
OGroupedList.Add(Groceries.Vegetables);
}
}
Finally, I added some items to each category in the page's constructor when creating the page:
public Page1()
{
InitializeComponent();
var bc = new OrganizedViewModel();
var index = 1;
foreach (var g in bc.OGroupedList)
{
g.Add(g.Category + $" {index++}");
g.Add(g.Category + $" {index++}");
g.Add(g.Category + $" {index++}");
g.Add(g.Category + $" {index++}");
}
BindingContext = bc;
}
And for me this is showing the lists with the item names and the group headers correctly. So whatever the problem is you're still seeing is somewhere else. The basic class structure and Xaml definition for the grouped ListView is now correct.
My two guesses:
Make sure the collections are being populated correctly.
Try changing to public class Groceries : ObservableCollection<string>. This is important if the Groceries list can change after the page is initially rendered.
EDIT 3
Got to the bottom of it. Here are comments that should get you going:
There are subtle differences between {Binding .} and {Binding}. In your case, {Binding} works but {Binding .} does not.
A more common case is where the elements are objects, not strings, so something like this also solves it:
public class GroceryItem
{
public string Name { get; set; }
}
...
public class Groceries: ObservableCollection<GroceryItem>
{
...
This will, of course, require changes in adding to the Groceries collection and the Label needs to be Text="{Binding Name}".
When I tried #2, Visual Studio was causing problems with x:DataType after making that change. Its detection of binding contexts in the IDE is not the best, so I had to delete that line.
Be careful when using static collections here. If you add items, organize, go back, and organize again, the app will crash because it tries to re-add the fruits and vegetables collections.
According to your code, for the OAllLists to be displayed, you can create OAllLists like this:
OAllLists = new ObservableCollection<Categories>
{
new Categories("FruitList"){"aaa","bbb","ccc},
new Categories("VegetableList"){"ddd","eee"}
};
Among them, "FruitList" is the type in the Categories class, and "aaa" is the string array you added in it. You can check this link for details (https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/listview/customizing-list-appearance#grouping)
Related
I am binding my ListView to my Realm database, but it doesn't refresh it when I add a new item into it. I am adding new items using another page.
I am using here as reference: https://help.syncfusion.com/xamarin/sflistview/mvvm#binding-itemssource
My Model:
public class Category : RealmObject
{
[PrimaryKey]
public string CategoryID { get; set; } = Guid.NewGuid().ToString();
public string CategoryTitle { get; set; }
public string CategoryDetails { get; set; }
[Backlink(nameof(Note.CategoryOfNote))]
public IQueryable<Note> Notes { get; }
public string CategoryIcon { get; set; }
public bool IsExpanded { get; set; }
}
My XAML file containing ListView
<ContentPage.BindingContext>
<vm:MainViewModel />
</ContentPage.BindingContext>
<ContentPage.ToolbarItems>
<ToolbarItem Text="NEWCAT"
Clicked="NewCat_Clicked"/>
</ContentPage.ToolbarItems>
<ContentPage.Content>
<ListView x:Name="categoryList"
ItemsSource="{Binding Categories}"
ItemTapped="ListView_ItemTapped"
HasUnevenRows="True">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Vertical"
HorizontalOptions="FillAndExpand"
Padding="10"
Spacing="10">
<Label Text="{Binding Path=CategoryTitle}"
FontSize="Medium"/>
<StackLayout IsVisible="{Binding IsExpanded}"
Orientation="Horizontal"
HorizontalOptions="CenterAndExpand">
<Button Text="Notes"
Clicked="NotesButton_Clicked" />
<Button Text="Edit"
Clicked="EditButton_Clicked"/>
<Button Text="Delete"
Clicked="DeleteButton_Clicked"/>
</StackLayout>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentPage.Content>
And my ViewModel
(DBServices.GetCategories is a static method that returns a collection of categories from Realm DB. And BaseViewModel implements INotifyPropertyChanged)
class MainViewModel : BaseViewModel
{
private Category _oldCategory;
public MainViewModel()
{
RefreshCategories();
}
private void RefreshCategories()
{
Categories = new ObservableCollection<Category>(DBServices.GetCategories());
}
private ObservableCollection<Category> _Categories;
public ObservableCollection<Category> Categories
{
get
{
return _Categories;
}
set
{
_Categories = value;
OnPropertyChanged("Categories");
}
}
}
This is OnPropertyChanged method in BaseViewModel class
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
When I click to the toolbar button, it pushes a new form page to create a new category. Then it just adds that category to the Realm DB. Afterwards it pops itself. But I see no change in my ListView.
EDIT: I can see new items in ListView when I restarted the app. But I want it to be listed as I add them.
You could use the Xamarin Forms MessagingCenter to tell the ListView it needs to update.
On the page where the new category is added to the Realm DB, at some point after the category is added but before the page is popped, you would send a message to tell the ListView page to update. This may look something like:
MessagingCenter.Send(this, "Update listview");
In your MainViewModel you'll need to subscribe to the message and then act upon it. This could be something like:
MessagingCenter.Subscribe<AddCategoryPage>(this, "Update listview", (sender) =>
{
RefreshCategories();
});
Two items to note when using the MessagingCenter.
The message passed must be identical between the sender and subscriber. In this case the message is "Update listview". Because it must be identical, you would probably want to define the message in separate class so it can be referenced by both the sender and subscriber.
When subscribing to a message you have to make sure that you unsubscribe. A popular pattern is to subscribe when the page appears and unsubscribe when the page disappears. A different pattern may work better for you though.
On a side note, you shouldn't need the full property definition for Categories. The ObservableCollection will handle the notification when items are added or removed. Just set Categories = new ObservableCollection<Category>(); once and then only add or remove items. You don't need to new it up on every DB call.
To test the XAML and ViewModel only, try replacing DBServices.GetCategories() with the following in the ViewModel:
public static IEnumerable<Category> TestCategories
{
get
{
yield return new Category { CategoryTitle = "title 1" };
yield return new Category { CategoryTitle = "title 2" };
}
}
...
public MainViewModel()
{
Categories = new ObservableCollection<Category>(TestCategories);
}
DBServices.GetCategories() should also be tested for expected output, not shown here.
I have a repeater view which's item source is an observable collection of view models and which has a data template. Now I want to use the index of the specific item to be used inside the data template. Is there any way to achieve this?
NOTE: The repeater view is part of UXDivers.Artina Library, but it should expose the same API as a listview and thus a solution that would work for a listview, would probably also work for the repeater.
Here is the code I have so far:
Xaml:
<ctlRep:Repeater
ItemsSource="{ Binding ListItems }"
Padding="10, 10"
Spacing="10"
Orientation="Vertical">
<ctlRep:Repeater.ItemTemplate>
<DataTemplate>
<elements:StructuredVideoDescriptionItemTemplate />
</DataTemplate>
</ctlRep:Repeater.ItemTemplate>
</ctlRep:Repeater>
Viewmodel:
public class VideoDescriptionStructureListItem : ObservableObject
{
public string Title { get; set; }
public bool IsNumberic { get; set; }
public ObservableCollection<string> ListItems { get; set; }
}
Inside the data template I would like to have access to the ListItems item (which is a string) and the index of the specific item.
It may not be the solution you are looking for, but I have solved the exact issue lately with the following strategy
Introduce an intermediate object that contains a property for an index
Build an ObservableCollection of those objects instead of string objects and assign the correct indices
Bind to the string and the index instead of the direct object
The class may look like this:
class VideoDescriptionViewModel
{
public string Description { get; private set; }
public int Index { get; private set; }
public VideoDescriptionViewModel(string description, int index)
{
Description = description;
Index = index;
}
}
and you can build the instances like
var videoDescriptionViewModels = videoDescriptions.Select((description, index) => new VideoDescriptionViewModel(description, index));
and bind it from your XAML
<elements:StructuredVideoDescriptionItemTemplate Description="{Binding Description}"
Id="{Binding Id}" />
I have a few Problems with databinding in WPF.
I have a ListBox which has a binding to a BindingList.
<ListBox x:Name="SampleListBox" ItemsSource="{Binding List1}" ItemContainerStyle="{StaticResource ListBoxStyle}" BorderThickness="0" SelectedIndex="0" Margin="0">
<ListBox.ItemTemplate>
<DataTemplate >
<Border x:Name="border" Width="185">
<TextBlock Text="{Binding name}"/>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Till here, everything works fine. Now I have a DataGrid which should be linked to another BindingList and display some strings of it. So for example, if the first item of the ListBox is selected, the grid should show data of the first item of the second list.
I know, how it would work if both, the ListBox and the Grid get the data from the same list, but I have no idea, what to do, if this is not possible and there are two different lists.
You could bind SelectedIndex for the ListBox control to an property of type Int (Property1) in your ViewModel.
Also two-way bind SelectedItem in the DataGrid to another property (Property2) of the second list type.
In the setter for the Property1, change Property2 to be the item at the index of Property1 - i.e. List2[Property1]. It should change the selected item in the DataGrid.
So you want to use the listbox to, essentially, set a filter on the grid?
Note that LBItem and ViewModel below need to implement INotifyPropertyChanged and fire their PropertyChanged events when properties change, or none of this will work. But I'm leaving out the boilerplate for clarity.
Lots of ways to do that.
C#
public class LBItem {
public ViewModel Parent { get; set; }
public IEnumerable<String> SubItems {
get {
return Parent.List2.Where( /* filter items here */ );
}
}
}
public class ViewModel {
//
public ObservableCollection<LBItem> LBItems { get; set; }
public LBItem SelectedLBItem { get; set; }
public List<String> List2 { get; set; }
}
XAML
<ListBox
Name="MasterLB"
ItemsSource="{Binding LBItems}"
SelectedItem={Binding SelectedLBItem}"
/>
<DataGrid
ItemsSource="{Binding ElementName=MasterLB, Path=SelectedItem.SubItems}"
/>
That will work whether or not you bind MasterLB.SelectedItem to a property on the ViewModel. But as long as you are binding MasterLB.SelectedItem, you could just as easily bind DataGrid.ItemsSource to SelectedLBItem.SubItems on the ViewModel, like so:
<DataGrid
ItemsSource="{Binding Path=SelectedLBItem.SubItems}"
/>
But the ElementName binding is handy for a lot of things, so I'm giving you both.
You could also do something like this:
C#
public class LBItem {
public IEnumerable<String> Filter(IEnumerable<String> fullList) {
return fullList.Where( /* filter items here */ );
}
}
public class ViewModel {
public ObservableCollection<LBItem> LBItems { get; set; }
private LBItem _selectedItem;
public LBItem SelectedLBItem {
get { return _selectedItem; }
set {
_selectedItem = value;
List2Filtered = (null == _selectedItem)
? new List<String>()
: _selectedItem.Filter(List2).ToList();
}
}
public List<String> List2 { get; set; }
public List<String> List2Filtered { get; set; }
}
XAML
<ListBox
Name="MasterLB"
ItemsSource="{Binding LBItems}"
SelectedItem={Binding SelectedLBItem}"
/>
<DataGrid
ItemsSource="{Binding List2Filtered}"
/>
I have a Pivotelement, so a Windows Phone Site with multiple sites and on every site should stand 5 menus. I have a List in which there every 5 menus and each menu of each site must be in a textbox:
//Site 1
textbox1_site1.text = list[0].menu1;
textbox2_site1.text = list[0].menu2;
...
//Site 2
textbox1_site2.text = list[1].menu1;
textbox2_site2.text = list[1].menu2;
...
//Site 3
textbox1_site3.text = list[2].menu1;
...
Here is where my list comes from.
public class Menus
{
public date Datum { get; set; }
public string menu1 { get; set; }
public string menu2 { get; set; }
...
}
public class list
{
public List<Menus> list { get; set; }
}
And I don't get it. how can I shorten this up in a loop like:
for(int i=0; i<5; i++)
{
textbox[i+1]_site[i+1] = list[i].menu1;
...
}
I know it's a beginner question and it's a little bit dumb but I don't get it.
from what i understand you have a list of values and want to display each value in a textbox. you can do this with a little data binding. You need to set the ItemsSource property of an ItemsControl to your data source (the list of values). After this all you have to do is modify the ItemsControl.ItemTemplate property. See if the code below helps. As you can see, everything is in the code-behind to keep it short.
//in ctor, after InitializeComponents()
DataContext = this;
//create the source of data (values to display in text boxes)
public List<string> Data{
get{
return new List<string>(){"item1", "item2"};
}
}
the rest is in the xaml file. Create an ItemsControl the gets the data from the Data property and displays it.
<ItemsControl ItemsSource="{Binding Data}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Mode=TwoWay}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
this should solve your problem
Current Setup
I have a custom class representing an installer file and some properties about that file, conforming to the following interface
public interface IInstallerObject
{
string FileName { get; set; }
string FileExtension { get; set; }
string Path { get; set; }
int Build { get; set; }
ProductType ProductType { get; set; }
Architecture ArchType { get; set; }
bool Configurable { get; set; }
int AverageInstallTime { get; set; }
bool IsSelected { get; set; }
}
My ViewModel has a ReadOnlyObservableCollection<IInstallerObject> property named AvailableInstallerObjects.
My View has a GroupBox containing the ItemsControl which binds to the aforementioned property.
<GroupBox Header="Products">
<ItemsControl ItemsSource="{Binding Path=AvailableInstallerObjects}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding Path=IsSelected}"
VerticalAlignment="Center" Margin="5"/>
<TextBlock Text="{Binding Path=FileName}" Margin="5" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</GroupBox>
The binding works correctly, except it's not user friendly. 100+ items are shown.
Need Help Here
I'd like to be able to use my collection of IInstallerObjects but have the View present them with the following ItemTemplate structure.
<GroupBox Header="Products">
<ItemsControl ItemsSource="{Binding Path=AvailableInstallerObjects}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding Path=IsSelected}"
VerticalAlignment="Center" Margin="5"/>
<TextBlock Text="{Binding Path=ProductType}" Margin="5" />
<ComboBox ItemsSource="{Binding Path=Build}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</GroupBox>
Basically I want to be able to group by the ProductType property, showing a list of the available products, with the ComboBox representing the available Build property values for IInstallerObjects of the ProductType.
I can use LINQ in the ViewModel to extract the groupings, but I have no idea how I'd bind to what I've extracted.
My research also turned up the possibility of using a CollectionViewSource but I'm not certain on how I can apply that to my current setup.
I appreciate your help in advance. I'm willing to learn so if I've overlooked something obvious please direct me to the information and I'll gladly educate myself.
If Build should be a collection type.
so your class should be structured like this as an example.
Public Class Customer
Public Property FirstName as string
Public Property LastName as string
Public Property CustomerOrders as observableCollection(OF Orders)
End Class
This should give you the expected results. Each item in the main items presenter will show first name last name and combobox bound to that customers orders.
I know it's simple but this should do.
All you have to do is declare a CollectionViewSource in your view and bind it to the ObservableCollection. Within this object you declare one or more GroupDescriptions which will split up the source into several groups.
Bind this source to the listbox, create a Template for the group description and you are done.
An example can be found here: WPF Sample Series – ListBox Grouping, Sorting, Subtotals and Collapsible Regions. More about CollectionViewSource can be found here: WPF’s CollectionViewSource
The description of your problem lead me to believe you are looking for some kind of colapsing / expanding / grouped / tree-view sort of thing.
XAML for the tree-view
<Window x:Class="WPFLab12.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:WPFLab12"
Title="MainWindow" Height="350" Width="525">
<Grid>
<GroupBox Header="Products">
<TreeView ItemsSource="{Binding Path=ProductTypes}">
<TreeView.Resources>
<HierarchicalDataTemplate
DataType="{x:Type loc:ProductType}"
ItemsSource="{Binding AvailableInstallerObjects}">
<TextBlock Text="{Binding Description}" />
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type loc:InstallerObject}">
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding Path=IsSelected}"
VerticalAlignment="Center" Margin="5"/>
<TextBlock Text="{Binding Path=FileName}" Margin="5" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
</GroupBox>
</Grid>
</Window>
What does that do? Well, it establishes a hierarchy of controls in the tree based on the type of data found. The first HierarchicalDataTemplate handles how to display the data for each class, and how they are related in the hierarchy. The second HierarchicalDataTemplate handles how to display each InstallerObject.
Code behind for the Main Window:
public partial class MainWindow : Window
{
public ReadOnlyObservableCollection<ProductType> ProductTypes
{
get { return (ReadOnlyObservableCollection<ProductType>)GetValue(ProductTypesProperty); }
set { SetValue(ProductTypesProperty, value); }
}
// Using a DependencyProperty as the backing store for ProductTypes. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ProductTypesProperty =
DependencyProperty.Register("ProductTypes", typeof(ReadOnlyObservableCollection<ProductType>), typeof(MainWindow), new UIPropertyMetadata(null));
public MainWindow()
{
this.InitializeComponent();
this.ProductTypes = new ReadOnlyObservableCollection<ProductType>(
new ObservableCollection<ProductType>()
{
new ProductType()
{
Description = "Type A",
AvailableInstallerObjects = new ReadOnlyObservableCollection<InstallerObject>(
new ObservableCollection<InstallerObject>()
{
new InstallerObject() { FileName = "A" },
new InstallerObject() { FileName = "B" },
new InstallerObject() { FileName = "C" },
})
},
new ProductType()
{
Description = "Type B",
AvailableInstallerObjects = new ReadOnlyObservableCollection<InstallerObject>(
new ObservableCollection<InstallerObject>()
{
new InstallerObject() { FileName = "A" },
new InstallerObject() { FileName = "D" },
})
}
});
this.DataContext = this;
}
}
This is totally cheating, though - normally the MainWindow.cs would not serve as the DataContext and have all this stuff. But for this example I just had it make a list of ProductTypes and populate each ProductType class with the InstallerObject instances.
Classes I used, note I made some assumptions and modified your class to suit this View Model better:
public class InstallerObject
{
public string FileName { get; set; }
public string FileExtension { get; set; }
public string Path { get; set; }
public int Build { get; set; }
public bool Configurable { get; set; }
public int AverageInstallTime { get; set; }
public bool IsSelected { get; set; }
}
public class ProductType
{
public string Description { get; set; }
public ReadOnlyObservableCollection<InstallerObject> AvailableInstallerObjects
{
get;
set;
}
public override string ToString()
{
return this.Description;
}
}
So, in MVVM, it seems to me that your current InstallerObject class is more of a Model layer sort of thing. You might consider transforming it in your ViewModel to a set of collection classes that are easier to manage in your View. The idea in the ViewModel is to model things similarly to how they are going to be viewed and interracted with. Transform your flat list of InstallerObjects to a new collection of hierarchical data for easier binding to the View.
More info on various ways to use and customize your TreeView: http://www.codeproject.com/Articles/124644/Basic-Understanding-of-Tree-View-in-WPF