This problem has been solved before, but I'm just not getting it with examples I'm finding online.
I have a class, lets say 'ClassA', this class has 2 string properties, 'Property1' and 'Property2' as well as an IEnumerable where 'ClassB' also has 2 properties. The list of ClassB will all be displayed in a nested treeview
I want these displayed in a treeview like so:
-ClassA[0]
ClassA.Property1
ClassA.Property2
-ClassA.ClassB Title
ClassB[0]
ClassB[1]
Etc.
+ClassA[1]
+ClassB[2]
It is my understanding that the way to accomplish this is to use HierarchicalDataTemplates however all examples I can find only tell me how to do:
-ClassA[0]
-ClassA.ClassB Title
ClassB[0]
ClassB[1]
Etc.
+ClassA[1]
+ClassB[2]
I cant figure out how to get the properties of ClassA in the template. Im thinking it'd be a DataTemplate on type ClassA but something isnt clicking.
Any help is greatly appreciated.
Thanks!
Well, I answered my own question, but I dont think it's the right way to go about this.
I used an itemtemplate on the treeview and then created another treeview inside of that template with another itemtemplate on it.
I can however, understand this when I look at it vs looking at the HierarchicalDataTemplates.
WPF:
<TreeView HorizontalAlignment="Left" Name="treeView1" VerticalAlignment="Top">
<TreeView.ItemTemplate>
<DataTemplate>
<TreeViewItem Header="{Binding FileName}">
<TextBlock Text="{Binding MetaData1}"/>
<TextBlock Text="{Binding MetaData2}"/>
<TreeViewItem ItemsSource="{Binding Mappings}" Header="Mappings">
<TreeViewItem.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="17"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Original}" Grid.Column="0"/>
<TextBlock Text="->" Grid.Column="1" Margin="3,0,3,0"/>
<TextBlock Text="{Binding Mapping}" Grid.Column="2"/>
</Grid>
</DataTemplate>
</TreeViewItem.ItemTemplate>
</TreeViewItem>
</TreeViewItem>
</DataTemplate>
</TreeView.ItemTemplate>
</TreeView>
Classes:
public class ClassA
{
public string MetaData1 { get; set; }
public string MetaData2 { get; set; }
public string FileName { get; set; }
public List<ClassB> Mappings { get; set; }
}
public class ClassB
{
public string Original { get; set; }
public string Mapping { get; set; }
}
Quick Implementation of my data structure:
new List<ClassA>
{
new ClassA
{
FileName = "ClassA 1",
MetaData1 = "Prop 1",
MetaData2 = "Prop 2",
Mappings = new List<ClassB>
{
new ClassB
{
Original = "BProp 1",
Mapping = "BProp 2"
}
}
},
new ClassA
{
FileName = "ClassA 2",
MetaData1 = "Prop 1",
MetaData2 = "Prop 2",
Mappings = new List<ClassB>
{
new ClassB
{
Original = "BProp 1",
Mapping = "BProp 2"
}
}
}
};
If anyone knows how I should have done this better (with HierachicalDataTemplates and DataTemplates Im open to seeing that code and improving upon this.
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.
I have an object(MyCals) that is type of Cal Collection. (Cal is an object).
The MyCals consists of 2 Cal, where each Cal has List of Events (Events does not has the INotifyPropertyChanged interface implemented, it has many properties like Summary, Id,...)
<TabControl ItemsSource="{Binding Path=MyCals, UpdateSourceTrigger=PropertyChanged}">
<TabControl.ItemTemplate>
<DataTemplate>
<!-- Tab Header -->
<TextBlock Text="{Binding Path=ProductID}" />
</DataTemplate>
</TabControl.ItemTemplate>
<!-- Content -->
<TabControl.ContentTemplate>
<DataTemplate>
<Grid>
<DataGrid AutoGenerateColumns="False"
HorizontalGridLinesBrush="LightGray"
ItemsSource="{Binding Path=Events, UpdateSourceTrigger=PropertyChanged}"
VerticalGridLinesBrush="LightGray">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Summary}" Header="Summary" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
Now the TabControl will show 2 tabs with the right header, but the Datagrid wont update when I change select the other tab. So if the First Tab is selected, that datagrid will be showed with the Data from first Cal, but when I select the second tab, data in the datagrid are not getting updated
(Same Events in the MyCals[1])
any help is really appreciated
I don't know how you implemented the ViewModel so I did a quick test and it works.
This is the code of the ViewModel:
class MainViewModel : AbstractViewModel
{
public MainViewModel()
{
this.MyCals = new ObservableCollection<Cal>();
this.MyCals.Add(new Cal());
this.MyCals.Add(new Cal());
}
public ObservableCollection<Cal> MyCals { get; set; }
}
public class Cal
{
public Cal()
{
this.ProductID = "Product " + Guid.NewGuid().ToString();
this.Events = new ObservableCollection<Event>();
this.Events.Add(new Event());
this.Events.Add(new Event());
}
public string ProductID { get; set; }
public ObservableCollection<Event> Events { get; set; }
}
public class Event
{
public Event()
{
this.Summary = "Event Summary " + Guid.NewGuid().ToString();
}
public string Summary { get; set; }
}
Please share a bit more about the code behind if my sample didn't help.
I'm building a series of admin forms as part of my application.
Many of the forms have this same scenario, and I'm wondering what the best way to manage this is...
Here's a screenshot of an example from the old winforms app. Not all of the forms are this simple, but I figure it's a good place to start.
The first field is always a combo box. After a user selects an item, the other fields populate with the associated data.
Currently, my ViewModel contains an ObservableCollection<circuit> (for this example). I'm not sure how to set up my bindings, given the master/detail scenario where your master (primary combobox) is also part of your detail (CircuitName field). I was thinking of pulling the combobox out and, after selecting an item, displaying a datagrid. Is there a better way? If I do go this route, I have read a few articles that indicate binding a combobox INSIDE the datagrid, while property binding the selectedValue prop is a total PITA. Obviously having a combobox would be required for a user to create/edit an existing CircuitName...
Your thoughts?
I'm not sure I fully understand, but if I was using MVVM Light, like I think you are:) And I wanted to replicated the screen above I would do something like this. But I'm probably not 100% understanding what you mean.
<Grid x:Name="LayoutRoot">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel Grid.ColumnSpan="2">
<TextBlock Text="Circuit Name"/>
<ComboBox SelectedValue="{Binding SelectedCircuit,Mode=TwoWay}" ItemsSource="{Binding Circuits}" DisplayMemberPath="Name"/>
</StackPanel>
<StackPanel Grid.Column="0" Grid.Row="1">
<TextBlock Text="Circuit Code"/>
<TextBlock Text="{Binding SelectedCircuit.Code}"/>
</StackPanel>
<StackPanel Grid.Column="1" Grid.Row="1">
<TextBlock Text="Voltage"/>
<TextBlock Text="{Binding SelectedCircuit.Voltage}"/>
</StackPanel>
<Button Grid.Row="2" Content="Okay"></Button>
<Button Grid.Row="2" Grid.Column="1" Content="Cancel"></Button>
</Grid>
using System.Collections.ObjectModel;
using GalaSoft.MvvmLight;
using MvvmLight1.Model;
namespace MvvmLight1.ViewModel
{
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
Circuits = new ObservableCollection<Circuit>
{
new Circuit {Code = "123123", Name = "Test1", Voltage = 2.2, Id = 1},
new Circuit {Code = "14224", Name = "Test2", Voltage = 3.2, Id = 2},
new Circuit {Code = "54234", Name = "Test3", Voltage = 4.2, Id = 3},
};
}
public ObservableCollection<Circuit> Circuits { get; set; }
private Circuit _selectedCircuit;
public Circuit SelectedCircuit
{
get { return _selectedCircuit; }
set { _selectedCircuit = value;
RaisePropertyChanged("SelectedCircuit");
}
}
}
}
namespace MvvmLight1.Model
{
public class Circuit
{
public int Id { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public double Voltage { get; set; }
}
}
Answer was really simple actually.
I have my ObservableCollection<Circuit> Circuits that's loaded in the VM Constructor. I created a ObservableCollection<Circuit> SelectedCircuit. My xaml then looks like this:
<ComboBox ItemsSource="{Binding Configs}"
SelectedValue="{Binding SelectedConfig, Mode=TwoWay}"
DisplayMemberPath="SwitchCode"
/>
<toolkit:DataForm CurrentItem="{Binding SelectedConfig}" AutoEdit="False" />
Given a list of objects containing two properties (IdentityType and Name) in the format:
IdentityType | Name
A | One
A | Two
A | Three
B | Four
B | Five
C | Six
Is there a way to declaratively databind that so the accordion displays like this?
A
- One
- Two
- Three
B
- Four
- Five
C
- Six
So far the best I can get is a panel header for each item, like so:
<toolkit:Accordion ItemsSource="{Binding Path=Identities}" Grid.Row="2" SelectionMode="ZeroOrMore">
<toolkit:Accordion.ItemTemplate>
<DataTemplate >
<TextBlock Text="{Binding IdentityType, Converter={StaticResource EnumDescriptionConverter}}"/>
</DataTemplate>
</toolkit:Accordion.ItemTemplate>
<toolkit:Accordion.ContentTemplate>
<DataTemplate>
<StackPanel Margin="5" Orientation="Horizontal">
<TextBlock Text="{Binding Name}" Foreground="White" />
</StackPanel>
</DataTemplate>
</toolkit:Accordion.ContentTemplate>
</toolkit:Accordion>
I'm new to Silverlight so I could be missing something blindingly obvious, but any help would be very much appreciated!
You can do this with a view model inbetween your model (the initail list) and your view (the markup).
Create a view model class with a Title and a NameCollection
Use LINQ (or a simple foreach) to translate your existing list of 6 entities to a list of 3 entites with 3, 2 and 1 Names in their name collection respectively.
Bind your Accordions ItemsSource to the collection of ViewModel objects.
Bind the text block in the your accordion items header template to your Title property
Add a repeating item control like ItemsControl to your content template of your accordion item
Bind your repeating item to the NamesCollection
Assuming your model is as follows...
public class Model
{
public string Title { get; set; }
public string Name { get; set; }
}
Your View Model structure should be...
public class ViewModel
{
public string Title { get; set; }
public List<string> Names { get; set; }
}
public class DataContextClass
{
public DataContextClass()
{
var modelData = new ModelData();
var query = from m in modelData.ModelCollection
group m by m.Title
into vm select new ViewModel { Title = vm.Key, Names = vm.Select(x => x.Name).ToList() };
ViewModelCollection = query.ToList();
}
public List<ViewModel> ViewModelCollection { get; set; }
}
Then your view can create an instance of your DataContextClass, assign it to it's own DataContext property and then use this markup...
<layout:Accordion ItemsSource="{Binding Path=ViewModelDataInstance.ViewModelCollection}" >
<layout:Accordion.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Title}" />
</DataTemplate>
</layout:Accordion.ItemTemplate>
<layout:Accordion.ContentTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding Path=Names}" />
</DataTemplate>
</layout:Accordion.ContentTemplate>
</layout:Accordion>
You can also use Tuple instead.
Code becomes :
public class DataContextClass{
public DataContextClass()
{
var modelData = new ModelData();
var query = from m in modelData.ModelCollection
group m by m.Title
into vm select Tuple.Create(vm.Key, vm.Select(x => x.Name).ToList() };
Collection = query.ToList();
}
public Tuple<string,List<string>> Collection { get; set; }
}
Xaml become :
<layout:Accordion ItemsSource="{Binding Path=ViewModelDataInstance.ViewModelCollection}" >
<layout:Accordion.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Item1}" />
</DataTemplate>
</layout:Accordion.ItemTemplate>
<layout:Accordion.ContentTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding Path=Item2}" />
</DataTemplate>
</layout:Accordion.ContentTemplate>
I hope it helps
Regard this image:
alt text http://img25.imageshack.us/img25/9743/timetablepo.png
The TimeTableViewModel of this user interface is this:
public string SchoolclassCodeMonday {get;set;}
public string SchoolclassCodeTuesday {get;set;}
public string SchoolclassCodeWednesday {get;set;}
public string SchoolclassCodeThursday {get;set;}
public string SchoolclassCodeFriday {get;set;}
public string SchoolclassCodeSaturday {get;set;}
public string SchoolclassCodeSunday {get;set;}
The above would work when I would display only the properties as string in a textbox.
But what I want is to bind each combox to a mutual ObservableCollection
SchoolclassCodes and the SelectedItem aka DisplayMember of the ComboBox must somehow be
MAPPED to one of the 7 above Properties AND the SelectedItem if retrieved must supply a row
of all 7 schoolclass selected in the combobox.
Or what I actually want just in other words ;-)
Display in the ComboBox the SchoolclassCodes list, set the Value of SelectedItem.SchoolclassCode"WeekdayName" to the Value of the selected ComboboxItem.SchoolclassCode
Well there are some ideas I have but all lack some experience to make them fully working.
I could add to the TimeTableViewModel for each Property a ObservableCollection
SchoolclassCodes but that seems very redundant to me. Why should I hold 7 lists for ONE row when each cell has the same list with the same items in it ?
Any suggestions concerning the ViewModels structure and the binding in Wpf are welcome :)
UPDATE: My SchoolclassCodes list is dynamically created, so I there is no possibility about static binding or hardcode string items in XAML...
UPDATE2:
OK I tried to make it working with MVVM:
I had to change the ObservableCollection ClassCodes
to ObservableCollection SchoolclassCodes as the Schoolclass object
has a reference to Pupil class with strings thats not possible.
Schoolclass.cs:
public string SchoolclassCode {get;set;}
...
TimeTableWeekViewModel.cs:
public ObservableCollection<Schoolclass> SchoolclassCodes
{
get { return _schoolclassCodes; }
set
{
_schoolclassCodes = value;
this.RaisePropertyChanged("SchoolclassCodes");
}
}
XAML:
How must the binding look like NOW because the SchoolclassCodes is not found by wpf ?
I recommend that you look into the MVVM (Model-View-ViewModel) pattern if you are doing anything remotely complicated. Also, it is useful to implement INotifyPropertyChanged in your model/viewmodel (e.g. the MyWeek class in my example below). That will notify any other bindings whenever one of your SchoolclassCode<Day> properties is changed.
Here is some simple sample code to get you started:
using System.Collections.ObjectModel;
using System.Linq;
namespace BindingSample
{
public partial class Window1
{
public Window1()
{
InitializeComponent();
SchoolclassCodes = new ObservableCollection<string>(
Enumerable.Range(1, 10).Select(i => "Code #" + i));
MyWeeks = new ObservableCollection<MyWeek>(
Enumerable.Range(1, 5).Select(i => new MyWeek() {SchoolclassCodeMonday = SchoolclassCodes.First()}));
DataContext = this;
}
public ObservableCollection<string> SchoolclassCodes { get; private set; }
public ObservableCollection<MyWeek> MyWeeks { get; private set; }
}
public class MyWeek
{
public string SchoolclassCodeMonday { get; set; }
public string SchoolclassCodeTuesday { get; set; }
public string SchoolclassCodeWednesday { get; set; }
public string SchoolclassCodeThursday { get; set; }
public string SchoolclassCodeFriday { get; set; }
public string SchoolclassCodeSaturday { get; set; }
public string SchoolclassCodeSunday { get; set; }
}
}
<Window x:Class="BindingSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Window.Resources>
<CollectionViewSource x:Key="ClassCodes" Source="{Binding SchoolclassCodes}" />
</Window.Resources>
<ListView ItemsSource="{Binding MyWeeks}">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn Header="Monday">
<GridViewColumn.CellTemplate>
<DataTemplate>
<ComboBox SelectedValue="{Binding SchoolclassCodeMonday}"
ItemsSource="{Binding Source={StaticResource ClassCodes}}"
IsSynchronizedWithCurrentItem="False" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<!-- Other columns here... -->
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
</Window>
Since the columns must be auto generated, putting this code in the appropriate place (Loaded event?) should do what you want :
ObservableCollection<String> Collection = GetCollection();
foreach (DataGridComboBoxColumn column in DataGrid1.Columns.OfType<DataGridComboBoxColumn>())
{
column.ItemsSource = Collection;
}
Of course, some modifications must be applied!
You don't need to mess around with the DataGrid, and you certainly don't need to create any class with seven different properties that you have to implement property-change notification for. The Grid control makes calendars easy.
Your view model should have three classes: Month, Week, and Day. The Week class contains a list of Day objects, and the Month class has a list of Week objects. Each class should have a reference to its parent, i.e. Week in Day and Month in Week.
The Week class's constructor should initialize its Days property to be a list of 7 Day objects. The Month class's constructor has to have more specific logic in it for setting up its Weeks property; I'll leave that to you.
The Day object should expose these properties:
public DayOfWeek DayNumber { get; private set; }
public ObservableCollection<SchoolclassCode> Codes { get { return Week.Codes; } }
as well as a read/write string Code property that does property-change notification.
The Week object should expose:
public ObservableCollection<SchoolclassCode> Codes { get { return Month.Codes; } }
public IEnumerable<Day> Days { get; private set; }
public IEnumerable<string> Codes
{
get { return Days.Select(x => x.Code); }
}
You can then define a data template for presenting days:
<DataTemplate DataType="{x:Type local:Day}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="Sunday"/>
<ColumnDefinition SharedSizeGroup="Monday"/>
<ColumnDefinition SharedSizeGroup="Tuesday"/>
<ColumnDefinition SharedSizeGroup="Wednesday"/>
<ColumnDefinition SharedSizeGroup="Thursday"/>
<ColumnDefinition SharedSizeGroup="Friday"/>
<ColumnDefinition SharedSizeGroup="Saturday"/>
</Grid.ColumnDefinitions>
<ComboBox
Grid.Column="{Binding DayNumber}"
ItemsSource="{Binding Codes}"
SelectedValue="{Binding Code, Mode=TwoWay}"/>
</Grid>
</DataTemplate>
and weeks:
<DataTemplate DataType="{x:Type Week}">
<ItemsControl ItemsSource="{Binding Days}">
<ItemsControl.ItemTemplate>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
and months:
<DataTemplate DataType="{x:Type Month}">
<ItemsControl ItemsSource="{Binding Weeks}" Grid.IsSharedSizeScope="True">
<ItemsControl.ItemTemplate>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
Adding day names and week numbers to the above templates is straightforward.