I am working on trying to learn and understand WPF TreeView and Data Binding in XAML. I cannot seem to grasp the concept of data binding and your help is much appreciated.
The ISSUE is nothing displays in the TreeView. I am mostly interested in understanding how to correctly bind data in WPF to objects for the implementation of a tree view.
Concept: consider a SubjectList with a set of Subjects. Each Subject only has ONE student for the sake of this example
Expected Output in TreeView
Maths
Student 1
Science
Student 2
Arts
Student 3
My current XAML attempt is based on the tutorial read here http://blogs.microsoft.co.il/pavely/2014/07/12/data-binding-for-a-wpf-treeview/.
Following this I have the 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"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<TreeView x:Name="treeView" HorizontalAlignment="Left"
Height="284"
Margin="18,10,0,0"
VerticalAlignment="Top"
Width="115"
ItemsSource="{Binding SubjectList}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding SubjectList}"
DataType="{x:Type local:Subject}">
<TreeViewItem Header="{Binding SubjectName}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</StackPanel>
MainWindow function to add Student Objects into a StudentList:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Student s1 = new Student("Alex", 1);
Student s2 = new Student("Kevin", 2);
Student s3 = new Student("Sina", 3);
Student s4 = new Student("Evan", 4);
Subject a1 = new Subject("Maths", 1);
Subject a2 = new Subject("Science", 2);
Subject a3 = new Subject("Arts", 3);
a1.setStudent(s1);
a2.setStudent(s2);
a3.setStudent(s3);
Subjects list = new Subjects();
list.AddSubjects(a1);
list.AddSubjects(a2);
list.AddSubjects(a3);
DataContext = list;
}
}
Classes
class Subjects
{
private List<Subject> subjectList;
public List<Subject> SubjectList { get; set;}
public Subjects()
{
SubjectList = new List<Subject>();
}
public void AddSubjects(Subject s)
{
SubjectList.Add(s);
}
}
class Subject
{
private String subjectName;
private List<Student> studentList;
//accessor methods
public String SubjectName { get; set; }
public List<Student> StudentList { get; }
public Subject()
{
}
public Subject(string name, int id)
{
SubjectName = name;
StudentList = new List<Student>();
}
public void setStudent(Student aStudent)
{
StudentList.Add(aStudent);
}
}
class Student
{
public Student()
{
}
public Student(string name, int id)
{
StudentName = name;
StudentID = id;
}
private String studentName;
private int studentID;
//accessor methods
public String StudentName { get; set;}
public int StudentID { get; set; }
}
}
What am i doing wrong? Anyone who can point me in the right direction of better understanding the concept of data binding in WPF to a list of objects would be a massive help in my self study!
Update 1: setting DataContext = list; and removing the DataContext reference in XAML resolved the issue of having two View's defined.
Question 2:: I am still a bit confused with View's. I have added a List as a property of a Subject Class.
How would you retrieve the Student Name from the List of Student objects which are within a Subject Object in XAML? Do you require a View for any/every collection you wish to show in the TreeView? I wish to learn how these pieces all work together. Any further material or assistance greatly appreciated.
You are creating two instances of your Subjects view model, one in XAML
<Window.DataContext>
<local:Subjects/>
</Window.DataContext>
and one in code
Subjects list = new Subjects();
Adding items to the list instance in code behind won't add them to the instance in the DataContext.
Change your code like this:
var list = (Subjects)DataContext;
list.AddSubjects(a1);
...
Or remove the DataContext assignment from your XAML and write the code behind like this:
var list = new Subjects();
list.AddSubjects(a1);
...
DataContext = list;
That said, it may make sense to use ObservableCollection instead of List to notify about collection changes, e.g. while adding or removing subjects.
So I believe your confusion is rooted in the concept of binding so I will give a crack at trying to clarify it for you.
Take this code you posted:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<TreeView x:Name="treeView" HorizontalAlignment="Left"
Height="284"
Margin="18,10,0,0"
VerticalAlignment="Top"
Width="115"
ItemsSource="{Binding SubjectList}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding SubjectList}"
DataType="{x:Type local:Subject}">
<TreeViewItem Header="{Binding SubjectName}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</StackPanel>
In your code behind you set the datacontext to an instance of Subjects. That means the window's datacontext is Subjects.
Now as you go deeper into the xaml you reach the stackpanel. This inherits the same datacontext so the stackpanel's datacontext is still Subjects.
Now you get to the treeview. The treeview's datacontext is still Subjects which is why you are able to bind the ItemsSource property to SubjectList, a property on the Subjects class.
Now when you get to the TreeView.ItemTemplate, the datacontext is a single item from SubjectList, aka an instance of Subject.
The HierachicalDataTemplate is still using the same context of an instance of Subject. You can't bind the HierarchicalDataTemplate's ItemsSource to SubjectList because the Subject class does not have that property.
You can bind to StudentList though since the Subject class does have that property.
If you were to bind the ItemsSource to StudentList you could then inside the HierarchicalDataTemplate put a TextBox with the binding to subject name since inside of the HierachicalDataTemplate has the DataContext of a singular StudentItem
So finally the working code would look like this:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<TreeView x:Name="treeView" HorizontalAlignment="Left"
Height="284"
Margin="18,10,0,0"
VerticalAlignment="Top"
Width="115"
ItemsSource="{Binding SubjectList}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding StudentList}">
<TextBox Text="{Binding StudentName}"/>
<TextBox Text=" "/>
<TextBox Text="{Binding ID}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</StackPanel>
So all in all you were very close, but I hope this helps you understand what is going on when you go deeper into the bindings. If you are confused about anything leave a comment and I will try to make it clearer
Related
I am trying to using mvvm pattern with wpf to create an interface for a project previously did in win form.
In this project i have an object that contains some List<> that i have to show in real time on my interface with a combobox, the problem is that combobox don't change his values. I'm using the dll of mvvm fundation for implement NotifyPropertyChanged. I think to make some mistake but i don'y know where is it.
I've tried to do a simple code with only one list in viewmodel and without a model but the result doesn't change.
<Window x:Class="ProvaComboBox.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ProvaComboBox"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.DataContext>
<local:ViewModel />
</Grid.DataContext>
<Button Content="Generate" Command="{Binding Generate}"/>
<Button Content="Clear" Command="{Binding Clear}"/>
<ComboBox ItemsSource="{Binding Path=Word, Mode=OneWay}" />
</Grid>
</Window>
//view Model
class ViewModel:ObservableObject
{
private List<string> _word;
public List<string> Word
{
get { return _word; }
}
public ViewModel()
{
_word = new List<string>();
}
public ICommand Generate
{ get { return new RelayCommand(GenerateExecute); } }
void GenerateExecute()
{
_prova.Add("pippo");
_prova.Add("pluto");
RaisePropertyChanged("Word");
}
public ICommand Clear
{ get { return new RelayCommand(ClearExecute); } }
void ClearExecute()
{
_prova.Clear();
RaisePropertyChanged("Word");
}
}
//View:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
I think that the problem it's RaisePropertyChanged, but it work correctly with normal variables.
I've tryed also using ObservableCollection and it work, but i can't use it with real project.
(p.s. Its my first question in stack overflow, sorry if i did some mistake!)
use ObservableCollection like that
public ObservableCollection<string> Word
{
get => _word;
set
{
_word= value;
RaisePropertyChanged("Word");
}
}
and change the binding mode in your combobox xaml code from OneWay to TwoWay or just remove it to be something like
<ComboBox ItemsSource="{Binding Path=Word}" />
What I'm trying to achieve is somewhat easy in web applications but I have been struggling to do it in WPF.
I want to load a paragraph of text in WPF and replace some of its specific words with editable textboxes. How can I do that?
What is the best strategy for getting this in a right a clean way?
Update:
Consider the following text. I want to display it in WPF and instead of the bold words put some textboxes.
Do you know someone rich and famous? Is he confident, popular, and
joyful all of the time—the epitome of mainstream success? Or,
on the other hand, is he stressed, having second thoughts about his life choices, and unsure about the meaning of his life?
WPF and XAML, unlike HTML, are all about data.
The best way to think and reason about any XAML-based UI is to think about the data that you need to show and interact with.
In this case:
public class Word
{
public string Value { get; set; }
public bool IsEditable { get; set; }
}
would represent each of our words. Then you just need a List of these:
public class ViewModel
{
public List<Word> Words { get; private set; }
public ViewModel()
{
var editableWords = new[] { "on", "of" };
var text = "Do you know someone rich and famous? Is he confident, popular, and joyful all of the time—the epitome of mainstream success? Or, on the other hand, is he stressed, having second thoughts about his life choices, and unsure about the meaning of his life?";
this.Words =
text.Split(' ')
.Select(x => new Word
{
Value = x,
IsEditable = editableWords.Contains(x.ToLower())
})
.ToList();
}
}
Notice how I'm turning the text into a List<Word> and setting IsEditable where desired.
Now it's just a matter of using an ItemsControl to show these:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVis"/>
</Window.Resources>
<ItemsControl ItemsSource="{Binding Words}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Margin="5,2,5,2">
<TextBlock Text="{Binding Value}"
VerticalAlignment="Center"/>
<TextBox Text="{Binding Value}"
Visibility="{Binding IsEditable, Converter={StaticResource BoolToVis}}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>
Finally, set the DataContext to an instance of our ViewModel:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
}
}
Result:
Notice that I'm not "touching" the UI in code at all, this is all just simple, simple properties and DataBinding.
i have this declaration:
public ObservableCollection<SomeType> Collection { get; set; }
i tried something, like:
myListBox.ItemsSource = Collection[0];
to show the first item of Collection in the Listbox control, but it gives error.
how will i do this? what change should i make on the right side?
You need to bind the ItemSource to your collection, and then set the selected index to the one you want (0 in your case)
Here's the smallest example. XAML:
<Window x:Class="WPFSOTETS.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"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid>
<ComboBox ItemsSource="{Binding Collection}" SelectedIndex="2"></ComboBox>
</Grid>
</Window>
Code behind:
using System.Collections.ObjectModel;
using System.Windows;
namespace WPFSOTETS
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
public ObservableCollection<string> Collection
{
get
{
return new ObservableCollection<string> {"one","two","three"};
}
}
}
}
I set the index to 2, just for fun, but you can play with it.
As of comment, if you want to set this in the codebehind, you'll have to name your control, and then you can use it from your codebehind and do your binding and setting there. You can have a look at this answer for example.
I have an ObservableCollection<IRuleCondition> that I want to display - the IRuleCondition interface is used by 2 different classes I want to display, a RuleCondition that simply displays one rule condition (info such as priority, property to check and so on), and a RuleConditionGroup, that can contain 2 or more RuleConditions, grouped in such a way that any of the conditions could match, or all etc.
In the XAML I was wondering is there a way to display a different ListView.ItemTemplate depending on what the type is that it encounters in the ObservableCollection<IRuleCondition>? Or would I need to implement two different ObservableCollections?
Here is a simple example of how this works
This is how the objects are defined
public interface Person
{
string Name { get; set; }
}
public class Manager : Person
{
public string Name { get; set; }
}
public class Employee : Person
{
public string Name { get; set; }
public string ManagerName { get;set;}
}
This is the MainWindow code behind
public partial class MainWindow : Window
{
ObservableCollection<Person> mPeople = new ObservableCollection<Person>();
public ObservableCollection<Person> People
{
get
{
return mPeople;
}
}
public MainWindow()
{
DataContext = this;
mPeople.Add( new Employee{ Name = "x" , ManagerName = "foo"});
mPeople.Add( new Manager { Name = "y"});
InitializeComponent();
}
}
This is the MainWindow XAML
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:my="clr-namespace:WpfApplication1"
Title="MainWindow"
Height="350"
Width="525">
<Window.Resources>
<DataTemplate DataType="{x:Type my:Employee}">
<StackPanel Background="Green" Width="300">
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding ManagerName}" />
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type my:Manager}">
<StackPanel Background="Red"
Width="300">
<TextBlock Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
</Window.Resources>
<Grid>
<ListBox ItemsSource="{Binding People}"></ListBox>
</Grid>
</Window>
As you can see there are two datatemplates one for Manager and one for Employee
And this is how the crappy output looks like. Notice the green and red background and extra field displayed for the Employee compared to the manager
Just define two different DataTemplates in the Resources section, one for each RuleCondition type.
1) Create your two different data templates, just as you say you've already done.
2) Create a custom DataTemplateSelector to choose the appropriate template.
One of your comments states that you're getting an error from your DataTemplateSelector. Verify that you're implementing the class correctly, perhaps paste your implementation. It should be fairly small and straightforward.
I'm having trouble understanding how to databind my Songs List<> to a ListBox without needing to set the ItemsSource in the code behind.
It works though, but I would really like to see the List working in the liveview Designer.
namespace App5
{
class SongsData
{
public string Title { get; set; }
public string Lyrics { get; set; }
}
}
And in my MainPage.xaml.cs:
public MainPage()
{
this.InitializeComponent();
List Songs = new List();
Songs.Add(new SongsData() { Title = "Your Song", Lyrics = "It's a little bit funny.." });
Songs.Add(new SongsData() { Title = "Rocket Man", Lyrics = "I'm the Rocket Maaaan.." });
SongsListBox.ItemsSource = Songs;
}
And in the XAML I have a basic ListBox:
<ListBox>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Title}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Can a friendly person please help me understand what to change - and hopefully why - to get the songs title to show in the ListBox in the liveview Designer of Visual Studio?
With the above I have to Debug the program to see the song titles in the ListBox.
Many thanks in advanced.
You basically need to apply the DesignData build time action to your data files. A very comprehensive walkthrough can be found at msdn.
A quick and simple solution is to move your ListBox to a new UserControl, put the list initialization in the UserControl's constructor, and then add an instance of the UserControl to your main form.
Example:
SongListControl.cs :
namespace App5
{
public parital class SongListControl : userControl
{
this.InitializeComponent();
List Songs = new List();
Songs.Add(new SongsData() { Title = "Your Song", Lyrics = "It's a little bit funny.." });
Songs.Add(new SongsData() { Title = "Rocket Man", Lyrics = "I'm the Rocket Maaaan.." });
SongsListBox.ItemsSource = Songs;
}
}
SongListControl.xaml :
<UserControl x:Class="App5.SongListControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<ListBox>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Title}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</UserControl>
Then in your main window :
<Window x:Class="App5.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:app="clr-namespace:App5"
Title="MainWindow" Height="350" Width="525">
<Grid>
<app:SongListControl />
</Grid>
</Window>
When you build the project, the constructor initialization will occur in MainWindow preview.