I want to bind my Datatemplate to 2 Datasources, one datasource that will actually define what is in the ListBox and other that will determine how many ListBoxes are there and what Items in the Listbox are selected\checked.
I have following XAML
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate x:Key="TokenListTemplate">
<StackPanel Orientation="Horizontal">
<CheckBox x:Name="chkToken" IsChecked="{Binding Path=IsSelected, Mode=TwoWay}">
<TextBlock Text="{Binding Path=Text}" />
</CheckBox>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="ItemTemplate">
<Border BorderThickness="1">
<StackPanel Margin="3">
<TextBlock Text="{Binding Path=Header}"/>
<ListBox ItemTemplate="{StaticResource TokenListTemplate}"
ItemsSource="{Binding Path=Tokens}" >
</ListBox>
</StackPanel>
</Border>
</DataTemplate>
</Window.Resources>
<Grid>
<ListBox ItemTemplate="{StaticResource ItemTemplate}"
ItemsSource="{Binding}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</Grid>
And this is the codebehind
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
ObservableCollection<DataEntity> _actualObjects;
List<Token> tokens1 = new List<Token>()
{
new Token("1"),
new Token("2"),
new Token("3"),
new Token("4")
};
List<Token> tokens2 = new List<Token>()
{
new Token("11"),
new Token("21"),
new Token("31")
};
_actualObjects = new ObservableCollection<DataEntity>()
{
new DataEntity(tokens1, "A", "1,2,3", 1),
new DataEntity(tokens1, "B", "2,3", 1),
new DataEntity(tokens2, "C", "21,31", 2)
};
DataContext = _actualObjects;
}
class DataEntity
{
public DataEntity(List<Token> tokens, string header, string tokenString, int entityTypeId)
{
Tokens = tokens;
Header = header;
TokenString = tokenString;
EntityTypeId = entityTypeId;
}
public List<Token> Tokens { get; set; }
public String Header { get; set; }
public String TokenString { get; set; }
public int EntityTypeId { get; set; }
}
public class Token
{
public bool IsSelected { get; set; }
public string Text { get; set; }
public Token(string text)
{
this.IsSelected = false;
this.Text = text;
}
}
}
It produces this
I don't want to inject token1 or token2 List into DataEntity object so in other words I want DataEntity constructor to be
public DataEntity(string header, string tokenString, int entityTypeId)
Listbox DataTemplate should select
tokens1 List as datasource for its LisBoxItems if
Dataentity.EntityTypeId = 1
tokens2 List as datasource for its LisBoxItemsif
DataEntity.EntityTypeId = 2
Also TokenString in DataEntity should be bound to items in the Listbox i.e. if Listbox shows 1 2 3 4
and DataEntity for this listbox has its TokenString value set to "1,2,3" then 1 2 3 should be checked in the listbox
I would recommend to create a ViewModel as a layer between your model and the view. In the ViewModel you can arrange the data to fit to the used controls without changing your model.
So the ViewModel could for example split the tokenString of the DataEntity into a list of tokens.
Just Google for MVVM (Model-View-ViewModel) for examples and furter explanations or look here on SO (like MVVM: Tutorial from start to finish?).
You're not thinking about this correctly. You need to create one class (some may call a view model) with the responsibility of providing all of the data that the view (or UI) will need. Therefore, you will need to have one property which holds a collection of type DataEntity (if I understand you correctly) to 'define what is in the outer ListBox' as you say.
Then you need a DataTemplate to describe what should be displayed for each item in the ListBox - your 'ItemTemplate' template. This DataTemplate should have another ListBox inside in which to display your Token objects. Your DataEntity should have something like this property in it:
public List<Token> Tokens
{
get
{
if (EntityTypeId == 1) return tokens1;
else if (EntityTypeId == 2) return tokens2;
}
}
You will then need another DataTemplate for your Token objects - your 'TokenListTemplate' template, but without the StackPanel... the inner ListBox replaces that, eg. if there are two Token objects in one DataEntity object, then that object would show two Checkboxes... you have correctly bound the IsChecked property to the Token.IsSelected property.
This may be complicated, but it is entirely possible. Just start with the first layer and get your DataEntity objects displayed in the outer ListBox using your 'ItemTemplate' template. Once that bit is ok, move on to the inner ListBox. Good luck.
Related
I've found a really strange quirk in WPF. If I specify a DataTemplate for an interface, it will work if defined inside an ItemsControl.ItemTemplate, but will not work if defined inside ItemsControl.Resrouces.
Concrete example:
I have a tree structure I want to represent. All items in the tree implement IHardware, but they do not necessarily have a common base type. If I define a HierarchicalDataTemplate for IHardware inside TreeView.ItemTemplate, everything works swimmingly. If I define the template inside TreeView.Resources, it never gets used/applied. The following shows the same data in 2 columns, the first column works as expected, the second column does not.
<Window x:Class="WPFInterfaceBinding.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:self ="clr-namespace:WPFInterfaceBinding"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<!-- Works -->
<Border
Grid.Column="0"
Background="Gray">
<TreeView
ItemsSource="{Binding Hardware}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate
DataType="{x:Type self:IHardware}"
ItemsSource="{Binding SubHardware}">
<TextBlock Text="{Binding Path=Name}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Border>
<!-- Doesn't work -->
<Border
Grid.Column="1"
Background="Gray">
<TreeView
ItemsSource="{Binding Hardware}">
<TreeView.Resources>
<HierarchicalDataTemplate
DataType="{x:Type self:IHardware}"
ItemsSource="{Binding SubHardware}">
<TextBlock Text="{Binding Path=Name}" />
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
</Border>
</Grid>
</Window>
Note that in the second column, nothing has changed except TreeView.ItemTemplate -> TreeView.Resources
Why is this the case? How can I get the template to work when inside Resources? I imagine I can work around this using a DataTemplateSelector, but first I'm curious if there's a way to actually get it working as expected.
Code behind, for completeness
using System.Windows;
namespace WPFInterfaceBinding
{
public partial class MainWindow : Window
{
public IHardware[] Hardware { get; private set; }
public MainWindow ()
{
Hardware = InitializeHardware();
InitializeComponent();
}
private IHardware[] InitializeHardware ()
{
return new Hardware[] {
new Hardware("Component 1", new Hardware[] {
new Hardware("Sub Component 1"),
new Hardware("Sub Component 2")
}),
new Hardware("Component 2", new Hardware[] {
new Hardware("Sub Component 3"),
new Hardware("Sub Component 4")
})
};
}
}
public class Hardware : IHardware
{
public string Name { get; set; }
public IHardware[] SubHardware { get; set; }
public Hardware ( string name, Hardware[] subHardware = null )
{
Name = name;
SubHardware = subHardware ?? new Hardware[0];
}
}
public interface IHardware
{
string Name { get; set; }
IHardware[] SubHardware { get; set; }
}
}
Additional information:
I can't simply use ItemTemplate because in my actual usage scenario there will be non-IHardware items mixed in using a CompositeCollection so I need multiple templates.
I can't change the types of the collections from IHardware to something concrete because I'm displaying data from code I don't control.
This is just example code, not representative of any design patterns actually in use.
Defining the template inside TreeView.Resources works just fine if the type is changed from IHardware to Hardware.
Turns out, WPF just doesn't like binding to interfaces. The only work around I could figure out was to use a DataTemplateSelector.
public class OHMTreeTemplateSelector : DataTemplateSelector
{
public HierarchicalDataTemplate HardwareTemplate { get; set; }
public DataTemplate SensorTemplate { get; set; }
public override DataTemplate SelectTemplate ( object item, DependencyObject container )
{
if ( item is IHardware ) return HardwareTemplate;
else if ( item is ISensor ) return SensorTemplate;
return base.SelectTemplate(item, container);
}
}
Though, for other reasons I ended up creating a separate ViewModel for the data that exposes it through concrete types, circumventing this issue.
Struggling with showing nested relationship in my TreeView. Here is the scenario:
In the database I have Category and Account tables. Each category can have zero or more sub-categories so this table has a nested relationship with itself. Each category/sub-category can have zero or more Account in it, there is a one-to-many relation between Category and Account. Simple, isn't it!
On top of my DB, I have EDMX, with Categories and Accounts entities in it and their associations as I mentioned above. For ease of understanding, I have renamed navigation properties so that Categories now has ParentCategory, ChildCategories and Accounts properties in it.
On top of EDMX, I have my ViewModel, which defines a public property named AllCategories. My TreeView will bind to this property. I initialize this property at the startup like this:
using (MyEntities context = new MyEntities())
Categories = context.Categories.Include(x => x.Accounts).ToList();
Finally I use the following HierarchicalDataTemplate to show this stuff:
<HierarchicalDataTemplate DataType = "{x:Type local:Category}" ItemsSource = "{Binding Path=ChildCategories}">
<TreeViewItem Header="{Binding Name}" ItemsSource="{Binding Accounts}" />
</HierarchicalDataTemplate>
<DataTemplate DataType = "{x:Type local:Account}">
<TextBlock Text="{Binding Name}" />
</DataTemplate>
This runs fine and shows categories, sub-categories and accounts in the tree, but the problem is that sub-categories show up not only under their parent category, but also at the root-level. This happens for categories of all depths. What am I doing wrong here?
Note: If I add .Where(x=>!x.ParentID.HasValue) in the VM, it shows only the root category and its immediate children, nothing else.
Edit
Here's what it currently looks like. Everything goes fine up to the dotted white line (I added that line manually for illustration; has nothing to do with WPF). After that, the sub-categories start repeating with their child sub-categories. This process continues over and over till the leaf sub-categories. I believe I understand what's going on here, but don't have a solution for it. For reference, this guy presents a solution of the problem, but he is using DataSets, whereas I'm working with EF and can't translate his solution into my scenario.
The idea is to connect your business data by ObservableCollections and leave your Hierarchical templates simple, so that the treeview won't show duplicate entries.
The sample code shows nested viewmodel relationship and the corresponding hierarchical templates. For simplification, the Root is an ObservableCollection (otherwise you would need to add INotifyPropertyChanged here and selective ItemsSource Binding in the TreeView)
<Window x:Class="MyWpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyWpf"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<HierarchicalDataTemplate DataType = "{x:Type local:RootItem}" ItemsSource = "{Binding Path=Categories}">
<TextBlock Text="{Binding Header}"></TextBlock>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType = "{x:Type local:CatNode}" ItemsSource = "{Binding Path=Items}">
<TextBlock Text="{Binding Header}"></TextBlock>
</HierarchicalDataTemplate>
</Window.Resources>
<Grid>
<TreeView ItemsSource="{Binding MyRoot}"/>
</Grid>
</Window>
namespace MyWpf
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
MyRoot = new ObservableCollection<RootItem>();
MyRoot.Add(new RootItem());
}
public ObservableCollection<RootItem> MyRoot { get; set; }
}
public class RootItem
{
public RootItem()
{
Categories = new ObservableCollection<CatNode>();
Categories.Add(new CatNode { Header = "Cat1" });
Categories[0].Items.Add("Item11");
Categories[0].Items.Add("Item12");
Categories.Add(new CatNode { Header = "Cat2" });
Categories[1].Items.Add("Item21");
Categories[1].Items.Add("Item22");
}
public string Header { get { return "Root"; }}
public ObservableCollection<CatNode> Categories { get; set; }
}
public class CatNode
{
public CatNode()
{
Items = new ObservableCollection<string>();
}
public string Header { get; set; }
public ObservableCollection<string> Items { get; set; }
}
}
I wonder how I can show design time data in Expression Blend that is located inside a SampleData.xaml using a CollectionViewSource? Before changing my code to use the CVS, I used an ObservableCollection. I was in the need to filter and sort the items inside there, thus I changed the code to use the CVS. Now my designer complains about not being able to fill the SampleData's NextItems with a proper structure to show up in Expression Blend. Here is some code I use inside the app:
MainViewModel.cs
class MainViewModel
{
public MainViewModel()
{
AllItems = new ObservableCollection<ItemViewModel>();
NextItems = new CollectionViewSource();
NextItems.Source = AllItems;
}
public CollectionViewSource NextItems
{
get;
private set;
}
public ObservableCollection<ItemViewModel> AllItems
{
get;
private set;
}
some functions to fill, filter, sort etc...
}
MainView.xaml:
<phone:PhoneApplicationPage
... some other stuff ...
d:DesignWidth="480"
d:DesignHeight="728"
d:DataContext="{d:DesignData SampleData/SampleData.xaml}">
<Grid
x:Name="LayoutRoot"
Background="Transparent">
<controls:Panorama>
<controls:PanoramaItem>
<ListBox ItemsSource="{Binding NextItems.View}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<Image Source="{Binding Image}" />
<StackPanel>
<TextBlock Text="{Binding FullName}" />
</StackPanel>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</controls:PanoramaItem>
</controls:Panorama>
</Grid>
</phone:PhoneApplicationPage>
SampleData.xaml
<local:MainViewModel
xmlns:local="clr-namespace:MyAppNamespace"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:swd="clr-namespace:System.Windows.Data;assembly=System.Windows" >
<local:MainViewModel.AllItems>
<local:ItemModel
FullName="Dummy"
Image="/Images/dummy.png" />
</local:MainViewModel.AllItems>
<local:MainViewModel.NextItems>
How to fill the CollectionViewSource's Source?
</local:MainViewModel.NextItems>
</local:MainViewModel>
So the question I can't find an answer to is how to fill the Source for NextItems in SampleDate.xaml? Any help would be much appreciated.
if you want to show sample data in the designer I would recommend you to do it from code. There are two ways of generating sample data for the Blend Designer or the VStudio designer:
From an XML file as you do.
From a c# class -> Best option
best option.
In WPF, in windows 8 and in WP7.5 and highger, you can access a propertie called:Windows.ApplicationModel.DesignMode.DesignModeEnabled making use of it you can seed your ObservableCollection from your view model:
public class MainViewModel
{
public MainViewModel()
{
AllItems = new ObservableCollection<ItemViewModel>();
if (DesignMode.DesignModeEnabled)
{
AllItems = FakeDataProvider.FakeDataItems;
}
NextItems.Source = AllItems;
}
public CollectionViewSource NextItems
{
get;
private set;
}
public ObservableCollection<ItemViewModel> AllItems
{
get;
private set;
}
}
In this way, if you change the model, you dont' have to regenerate an XML file, it's a little bit cleaner from a C# file. The FakeDataProvider is an static class where all design-time fake data are stored. So in you XAML the only thing you have to do is to bind your Listbox to the collection of your ViewModel.
I have a Datagrid control in my WPF application and I am trying to bind that control to an ObservableCollection property in my Main Window's class. The property I'm trying to bind to is defined as:
private ObservableCollection<RequestResult> m_SentRequests = new ObservableCollection<RequestResult>();
public ObservableCollection<RequestResult> SentRequests { get { return m_SentRequests; } }
My datagrid is in a group by which has the datacontext set to the MainWindow:
<GroupBox Header="Results" Height="275" HorizontalAlignment="Stretch" Margin="0,305,0,0" Name="grpResults" VerticalAlignment="Top" Width="712" DataContext="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=my:MainWindow, AncestorLevel=1}}">
<Grid>
<DataGrid AutoGenerateColumns="False" Height="246" HorizontalAlignment="Stretch" Margin="6,6,6,0" Name="dgResults" VerticalAlignment="Top" ItemsSource="{Binding Path=SentRequests}" DataContext="{Binding}" IsSynchronizedWithCurrentItem="True" />
</Grid>
</GroupBox>
The problem that I'm having is that in the properties window, after I select SentRequests as my ItemsSource, I still can't select the "Edit Property-Bound Columns" option. I get a "You must set ItemsSource before you can perform this action" dialog. I get the same error when selecting "Generate Columns" and "Remove Columns". It's as if I haven't set anything in the ItemsSource property for my Dialog.
I can set AutoGenerateColumns to true though and I see my data get bound though (however, not with the columns I want to show).
I'm very new to WPF and I'm just writing this as a quick test app for testing a windows service.
Any one know what I'm doing wrong here?
I don't believe you need the Path parameter within your itemSource. You should be able to just set the binding as ItemsSource={Binding SentRequests}
You can also bind to the grid item source in code for example if I create a dummy collection:
public class People
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Age { get; set; }
public string address { get; set; }
public string zip { get; set; }
}
and then populate it
this.AllPeople = new ObservableCollection<People>();
private void FillData()
{
People p1 = new People();
p1.FirstName = "John";
p1.LastName = "Doe";
p1.Age = "24";
p1.address = "123 Main Street";
p1.zip = "11111";
People p2 = new People();
p2.FirstName = "Jane";
p2.LastName = "Smith";
p2.Age = "36";
p2.address = "456 Water Street";
p2.zip = "22222";
People p3 = new People();
p3.FirstName = "Larry";
p3.LastName = "Williams";
p3.Age = "24";
p3.address = "785 Water Street";
p3.zip = "33333";
this.AllPeople.Add(p1);
this.AllPeople.Add(p2);
this.AllPeople.Add(p3);
}
I could then set the items source in the mainpage contsructor or method as:
this.gridviewname.ItemsSource = "AllPeople";
This is probably a result of some of the trickery that the designer does to render without constantly compiling (like skipping code-behind constructors). Try moving your collection to a separate class and use an instance of that as your DataContext (like an MVVM ViewModel). The other class should be able to initialize normally and provide the bound property to the designer.
Have you tryed without the DataContext tags? Both in GroupBox and DataGrid.
EDIT
something like this:
<GroupBox Header="Results" Height="275" HorizontalAlignment="Stretch" >
<Grid>
<DataGrid AutoGenerateColumns="False" Height="246" HorizontalAlignment="Stretch" Name="dgResults" VerticalAlignment="Top" ItemsSource="{Binding Path=SentRequests}" IsSynchronizedWithCurrentItem="True" />
</Grid>
</GroupBox>
I've been scratching my head on this one for a while now and am stumped at the moment.
The problem scenario is easier to explain as code so hopefully it speaks for itself. First of all, I have a silverlight application with the following in the XAML...
<UserControl x:Class="SilverlightApplication2.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="400" Height="300">
<UserControl.Resources>
<DataTemplate x:Key="icTemplate">
<ComboBox ItemsSource="{Binding StringsChild}" SelectedItem="{Binding SelectedItem}"/>
</DataTemplate>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<ItemsControl x:Name="ic" ItemTemplate="{StaticResource icTemplate}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<Button Click="Save" Grid.Row="1" Content="GO"/>
</Grid>
My code-behind looks like this...(all written in a single class file so that it's easy for you to copy it into your own project and compile)
namespace SilverlightApplication2
{
public partial class Page : UserControl
{
public ObservableCollection<SomeClass> StringsParent { get; set; }
public Page()
{
InitializeComponent();
StringsParent = new ObservableCollection<SomeClass>();
ic.ItemsSource = StringsParent;
}
private void Save(object sender, RoutedEventArgs e)
{
SomeClass c = new SomeClass();
c.StringsChild.Add("First");
c.StringsChild.Add("Second");
c.StringsChild.SetSelectedItem("Second");
StringsParent.Add(c);
}
}
public class SomeClass
{
public SelectableObservablecollection<string> StringsChild { get; set; }
public SomeClass()
{
StringsChild = new SelectableObservablecollection<string>();
}
}
public class SelectableObservablecollection<T> : ObservableCollection<T>
{
public SelectableObservablecollection()
: base()
{
}
public void SetSelectedItem<Q>(Q selectedItem)
{
foreach (T item in this)
{
if (item.Equals(selectedItem))
{
SelectedItem = item;
return;
}
}
}
private T _selectedItem;
public T SelectedItem
{
get
{
return _selectedItem;
}
set
{
_selectedItem = value;
OnPropertyChanged(new PropertyChangedEventArgs("SelectedItem"));
}
}
}
}
So let me explain...
I set out to write a generic way of creating an ObservableCollection that has a SelectedItem property on it so that when I bind the collection to a ComboBox for example, I can Bind the ComboBox's SelectedItem property to it.
However, for some reason, it does not seem to work when the ComboBox is effectively nested via an ItemTemplate. I effectively have a list of lists, a scenario which is simple enough that I'm lost as to what's wrong.
When you run the code you'll see that the templated ComboBox does pick up the correct items, but it's never set to a SelectedItem despite the binding.
I know it's rather long winded, but...any ideas?
Thanks alot
The debugger output actually gives you a hint to the problem:
System.Windows.Data Error: BindingExpression path error: 'SelectedItem' property not found on 'ExpressionElements.SomeClass' 'ExpressionElements.SomeClass' (HashCode=49044892). BindingExpression: Path='SelectedItem' DataItem='ExpressionElements.SomeClass' (HashCode=49044892); target element is 'System.Windows.Controls.ComboBox' (Name=''); target property is 'SelectedItem' (type 'System.Object')..
Because the Data context for the template is an instance of the SomeClass class, all you have to do is change the SelectedItem binding from SelectedItem to StringsChild.SelectedItem:
<DataTemplate x:Key="icTemplate">
<ComboBox ItemsSource="{Binding StringsChild}"
SelectedItem="{Binding StringsChild.SelectedItem}"/>
</DataTemplate>