So I finally decided to move from WinForms to WPF, and I'm having quite an interesting journey. I have a simple application in which I bind an ObservableCollection to a ListBox.
I have an Animal entity:
namespace MyTestApp
{
public class Animal
{
public string animalName;
public string species;
public Animal()
{
}
public string AnimalName { get { return animalName; } set { animalName = value; } }
public string Species { get { return species; } set { species = value; } }
}
}
And an AnimalList entity:
namespace MyTestApp
{
public class AnimalList : ObservableCollection<Animal>
{
public AnimalList() : base()
{
}
}
}
And finally here's my main window:
<Window x:Class="MyTestApp.Window3"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyTestApp"
Title="Window3" Height="478" Width="563">
<Window.Resources>
<local:AnimalList x:Key="animalList">
<local:Animal AnimalName="Dog" Species="Dog"/>
<local:Animal AnimalName="Wolf" Species="Dog"/>
<local:Animal AnimalName="Cat" Species="Cat"/>
</local:AnimalList>
</Window.Resources>
<Grid>
<StackPanel Orientation="Vertical" Margin="10,0,0,0">
<TextBlock FontWeight="ExtraBold">List of Animals</TextBlock>
<ListBox ItemsSource="{Binding Source={StaticResource animalList}, Path=AnimalName}"></ListBox>
</StackPanel>
</Grid>
Now when I run the application, I see the listbox populated with three items: "D", "o", and "g" instead of "Dog", "Wolf", and "Cat":
I have a strong feeling that I'm doing something stupid somewhere (AnimalList constructor maybe?) but I can't figure out what it is. Any help is appreciated.
You need to set the DisplayMemberPath (as opposed to the Path property in the binding).
<Grid>
<StackPanel Orientation="Vertical" Margin="10,0,0,0">
<TextBlock FontWeight="ExtraBold">List of Animals</TextBlock>
<ListBox ItemsSource="{Binding Source={StaticResource animalList}}" DisplayMemberPath="AnimalName"></ListBox>
</StackPanel>
</Grid>
Since you are binding to a list of Animal objects, DisplayMemberPath specifies the name of the property in the Animal class that you want to show up as a list item.
If the property is itself an object, you can use dot notation to specify the full path to the property you want displayed ie..
<ListBox ItemsSource="{Binding Source={StaticResource animalList}}" DisplayMemberPath="PropertyInAnimalClass.PropertyInTheChildObject.PropertyToDisplay" />
You're binding your listbox to the animalname. Instead you should bind your listbox to your collection:
<ListBox ItemsSource="{Binding Source={StaticResource animalList}}"></ListBox>
Notice that I've removed the path=AnimalName from the binding.
Now you will see the class name, since the ListBox doesn't know how to display an Animal and therefore it calls its ToString-method.
You can solve this by giving it an ItemTemplate like so:
<ListBox ItemsSource="{Binding Source={StaticResource animalList}}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding AnimalName}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Inside the itemtemplate your DataContext is an instance of Animaland you can then bind to the properties on that instance. In my example I have bound the AnimalName, but you basically construct any template you want using normal XAML-controls and binding to the different properties of your bound object.
Related
I have a scenario where I have multiple lists of data that is constructed programmatically in a loop. I want to display these lists side by side on the UI.
I setup a List of ObservableCollections of strings to contain the data. I am using the ListBox way of binding to the lists as shown here: Bind textbox list inside listbox in wpf
with this XMAL:
<ListBox Name="ListTwo" ItemsSource="{Binding Source=obs}" ... >
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Name="TextBoxList" Text="{Binding}" />
</DataTemplate>
</ListBox.ItemTemplate>
The problem is how do you bind a specific listbox to an specific index in the List? Specifically I want the 0th list to bind to List[0], 1st to bind to List[1], etc.
So took the below suggestion and tried to make things into a class. This is what I got, but the UI isn't showing the updates.
XAML:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
x:Class="NS.ClassMainWindow"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Window.Resources>
<ListView Grid.Row="1" Grid.Column="0" x:Name="RawBox" ItemsSource="{Binding Foo.Raw}" Background="LightGray" BorderThickness="2" BorderBrush="Black">
<ListView.ItemTemplate>
<DataTemplate>
<TextBox Name="RawItemsTextBoxList" Text="{Binding}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
DataClass:
public class FooClass
{
public List<ObservableCollection<string>> items;
public ObservableCollection<string> Raw { get => this.items[0]; set => this.items[0] = value; }
public ObservableCollection<string> Tier1 { get => this.items[1]; set => this.items[1] = value; }
public ObservableCollection<string> Tier2 { get => this.items[2]; set => this.items[2] = value; }
public ObservableCollection<string> Tier3 { get => this.items[3]; set => this.items[3] = value; }
public ObservableCollection<string> Tier4 { get => this.items[4]; set => this.items[4] = value; }
public FooClass()
{
this.items = new List<ObservableCollection<string>>();
for (int i = 0; i <= 4; i++)
{
this.items.Add(new ObservableCollection<string>());
}
}
}
And Assignment:
this.Foo.Raw = new ObservableCollection<string>(itemNames); // itemNames is a List<string>
I am very obviously missing something, but for the life of me can't see it. Fairly new to WPF so probably is a noob thing.
If you set the DataContext to an instance of a FooClass and then set the Raw property a non-empty collection like this:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var foo = new FooClass();
foo.Raw = new ObservableCollection<string>(new List<string> { "1", "2", "3" });
DataContext = foo;
}
}
...you should see the strings provided that you bind to the Raw property and set the Mode of the TextBox to either OneTime or OneWay:
<ListView Grid.Row="1" Grid.Column="0" x:Name="RawBox" ItemsSource="{Binding Raw}"
Background="LightGray" BorderThickness="2" BorderBrush="Black">
<ListView.ItemTemplate>
<DataTemplate>
<TextBox Name="RawItemsTextBoxList" Text="{Binding Mode=OneTime}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
I am writing a new user control. It needs to be able to display an ObservableCollection of items. Those items will have a property that is also an observable collection, so it is similar to a 2-d jagged array. The control is similar to a text editor so the outer collection would be the lines, the inner collection would be the words. I want the consumer of the control to be able to specify not only the binding for the lines, but also the binding for the words. The approach I have so far is as follows:
The user control inherits from ItemsControl. Inside this control it has a nested ItemsControl. I would like to be able to specify the binding path of this nested ItemsControl from the parent user control. The XAML for the UserControl is
<ItemsControl x:Class="IntelliDoc.Client.Controls.TextDocumentEditor"
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"
xmlns:local="clr-namespace:IntelliDoc.Client"
xmlns:con="clr-namespace:IntelliDoc.Client.Controls"
xmlns:data="clr-namespace:IntelliDoc.Data;assembly=IntelliDoc.Data"
xmlns:util="clr-namespace:IntelliDoc.Client.Utility"
xmlns:vm="clr-namespace:IntelliDoc.Client.ViewModel"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
mc:Ignorable="d"
x:Name="root"
d:DesignHeight="300" d:DesignWidth="300"
>
<ItemsControl.Template>
<ControlTemplate>
<StackPanel Orientation="Vertical">
<ItemsPresenter Name="PART_Presenter" />
</StackPanel>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemTemplate>
<DataTemplate >
<StackPanel Orientation="Horizontal">
<ItemsControl Name="PART_InnerItemsControl" ItemsSource="{Binding NestedBinding, ElementName=root}" >
<ItemsControl.Template>
<ControlTemplate>
<StackPanel Name="InnerStackPanel" Orientation="Horizontal" >
<TextBox Text="" BorderThickness="0" TextChanged="TextBox_TextChanged" />
<ItemsPresenter />
</StackPanel>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" >
<ContentControl Content="{Binding Path=Data, Mode=TwoWay}" />
<TextBox BorderThickness="0" TextChanged="TextBox_TextChanged" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
The code behind has this property declared
public partial class TextDocumentEditor : ItemsControl
{
public static readonly DependencyProperty NestedItemsProperty = DependencyProperty.Register("NestedItems", typeof(BindingBase), typeof(TextDocumentEditor),
new PropertyMetadata((BindingBase)null));
public BindingBase NestedItems
{
get { return (BindingBase)GetValue(NestedItemsProperty); }
set
{
SetValue(NestedItemsProperty, value);
}
}
...
}
The expected bound object will be as follows:
public class ExampleClass
{
ObservableCollection<InnerClass> InnerItems {get; private set;}
}
public class InnerClass : BaseModel //declares OnPropertyChanged
{
private string _name;
public string Name //this is provided as an example property and is not required
{
get
{
return _name;
}
set
{
_name = value;
OnPropertyChanged(nameof(Name));
}
}
....
}
public class ViewModel
{
public ObservableCollection<ExampleClass> Items {get; private set;}
}
The XAML declaration would be as follows:
<Window x:Class="IntelliDoc.Client.TestWindow"
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:sys="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d"
Title="TestWindow" Height="300" Width="300">
<DockPanel>
<TextDocumentEditor ItemsSource="{Binding Path=Items}" NestedItems={Binding Path=InnerItems} >
<DataTemplate>
<!-- I would like this to be the user defined datatemplate for the nested items. Currently I am just declaring the templates in the resources of the user control by DataType which also works -->
</DataTemplate>
</TextDocumentEditor>
</DockPanel>
In the end, I want the user control I created to provide the ItemsControl template at the outer items level, but I want the user to be able to provide the datatemplate at the inner items control level. I want the consumer of the control to be able to provide the bindings for both the Outer items and the nested items.
I was able to come up with a solution that works for me. There may be a better approach, but here is what I did.
First, on the outer ItemsControl, I subscribed to the StatusChanged of the ItemContainerGenerator. Inside that function, I apply the template of the ContentPresenter and then search for the Inner ItemsControl. Once found, I use the property NestedItems to bind to the ItemsSource property. One of the problems I was having originally was I was binding incorrectly. I fixed that and I changed the NestedItems to be a string. Also, I added a new property called NestedDataTemplate that is of type DataTemplate so that a user can specify the DataTemplate of the inner items control. It was suggested that I not use a UserControl since I don't inherit from a UserControl, so I will change it to a CustomControl. The code changes are below
public static readonly DependencyProperty NestedItemsProperty = DependencyProperty.Register("NestedItems", typeof(string), typeof(TextDocumentEditor),
new PropertyMetadata((string)null));
public static readonly DependencyProperty NestedDataTemplateProperty = DependencyProperty.Register("NestedDataTemplate", typeof(DataTemplate), typeof(TextDocumentEditor),
new PropertyMetadata((DataTemplate)null));
public DataTemplate NestedDataTemplate
{
get { return (DataTemplate)GetValue(NestedDataTemplateProperty); }
set
{
SetValue(NestedDataTemplateProperty, value);
}
}
public string NestedItems
{
get { return (string)GetValue(NestedItemsProperty); }
set
{
SetValue(NestedItemsProperty, value);
}
}
private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
if (((ItemContainerGenerator)sender).Status != GeneratorStatus.ContainersGenerated)
return;
ContentPresenter value;
ItemsControl itemsControl;
for (int x=0;x<ItemContainerGenerator.Items.Count; x++)
{
value = ItemContainerGenerator.ContainerFromIndex(x) as ContentPresenter;
if (value == null)
continue;
value.ApplyTemplate();
itemsControl = value.GetChildren<ItemsControl>().FirstOrDefault();
if (itemsControl != null)
{
if (NestedDataTemplate != null)
itemsControl.ItemTemplate = NestedDataTemplate;
Binding binding = new Binding(NestedItems);
BindingOperations.SetBinding(itemsControl, ItemsSourceProperty, binding);
}
}
}
I'm trying to get the databinding I need to work with a ListBox.
I've parsed some data from a text file to a ObservableCollection<ViewModel> but the data isn't updating in the ListBox.
Here's some information:
The data which is written to from the parser:
class MainData
{
private static ObservableCollection<GroupViewModel> groupModelList = new ObservableCollection<GroupViewModel>();
public static ObservableCollection<GroupViewModel> GroupModelList
{
get { return groupModelList; }
}
}
What GroupViewModel holds (not everything but it's all the same):
class GroupViewModel : INotifyPropertyChanged
{
private GroupModel groupModel;
public event PropertyChangedEventHandler PropertyChanged;
public GroupViewModel()
{
groupModel = new GroupModel();
}
public string Name
{
get { return groupModel.name; }
set
{
if (groupModel.name != value)
{
groupModel.name = value;
InvokePropertyChanged("Name");
}
}
}
...
}
And what GroupModel Holds:
class GroupModel
{
public string name { get; set; }
}
This is how the parser adds new items to the GroupModelView:
if (split[0] == "group")
{
currentGroup = new GroupViewModel();
currentGroup.Name = split[1];
MainData.GroupModelList.Add(currentGroup);
}
I created a ListBox in my WPF application with these XAML options:
<Window x:Class="SoundManager.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:SoundManager.ViewModels"
xmlns:vm2="clr-namespace:SoundManager.Code"
Title="MainWindow" Height="720" Width="1280">
<Window.Resources>
<vm:MainViewModel x:Key="MainViewModel" />
<vm2:MainData x:Key="MainData" />
</Window.Resources>
<ListBox Grid.Row="2" Height="484" HorizontalAlignment="Left" Margin="12,0,0,0" Name="lbFoundItems" VerticalAlignment="Top" Width="201" ItemsSource="{Binding Source={StaticResource MainData}, Path=GroupModelList/Name}" />
but for some reason the data isn't updating in the UI (new items aren't added visibly in the UI).
I've been just getting started with the MVVM pattern and databinding and I can't figure out what I'm doing wrong.
Thanks in advance!
GroupModelList/Name is not a valid property path here. Setting it like that does not make the ListBox show the Name property of the data items in the GroupModelList collection.
You would instead have to set the ListBox's DisplayMemberPath property:
<ListBox ItemsSource="{Binding Source={StaticResource MainData}, Path=GroupModelList}"
DisplayMemberPath="Name"/>
or set the ItemTemplate property:
<ListBox ItemsSource="{Binding Source={StaticResource MainData}, Path=GroupModelList}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Moreover, the GroupModelList property should not be static:
class MainData
{
private ObservableCollection<GroupViewModel> groupModelList =
new ObservableCollection<GroupViewModel>();
public ObservableCollection<GroupViewModel> GroupModelList
{
get { return groupModelList; }
}
}
Then you might have MainData as a property in your view model, and bind the ListBox like this:
<ListBox ItemsSource="{Binding Source={StaticResource MainViewModel},
Path=MainData.GroupModelList}" .../>
I am trying to figure out how to handle Combobox with complex objects.
I have the following 2 classes:
BackupVersion.cs
public class BackupVersion
{
public string Name { get; set; }
public BackupVersion() { }
public BackupVersion(string name)
{
Name = name;
}
}
SubsystemVersions.cs
public class SubsystemVersions : ObservableCollection<BackupVersion>
{
public SubsystemVersions()
{
Add(new BackupVersion("amit"));
Add(new BackupVersion("aaa"));
Add(new BackupVersion("ofir"));
}
}
I also have to following XAML window:
<Grid>
<StackPanel>
<StackPanel.Resources>
<local:SubsystemVersions x:Key="Backups"/>
</StackPanel.Resources>
<ComboBox Name="c1"
ItemsSource="{StaticResource Backups}"
Text="mmm"
DisplayMemberPath="Name"
SelectedValuePath="Name"
IsEditable="true"
IsReadOnly="true"/>
<TextBlock Text="{Binding ElementName=comboBox1, Path=SelectedItem}"/>
</StackPanel>
</Grid>
This way, in the code behind, I can get the selected string in the combobox using:
this.c1.SelectedValue.ToString()
My question is, how can I get back the original object i.e. the BackupVersion object?
Please also comment on the coding style, if I am doing something which is not common (for example, is that the best way to define and bind the collection?)
To get back the original object :
this.c1.SelectedItem;
Okay, I just don't get it. Please tell me why I get no items in my ListBox (should be the two strings "empty" and "stuff" right now ):
XAML:
<Window.DataContext>
<Windows:SettingsWindowModel x:Name="model"/>
</Window.DataContext>
<Window.Resources>
<DataTemplate x:Key="ListItemTemplate">
<ListBoxItem Content="{Binding}" />
</DataTemplate>
</Window.Resources>
<ListBox Name="listBoxActivities" SelectionChanged="ListBoxActivitiesSelectionChanged"
ItemsSource="{Binding Path=IgnoredActivities}"
HorizontalAlignment="Left" VerticalAlignment="Top" MinHeight="40" MinWidth="200"
Padding="5,100,5,0" Height="100" Margin="0,207,0,0" ItemTemplate="{StaticResource ListItemTemplate}" />
In SettingsWindowModel:
private ObservableCollection<String> _ignoredActivities;
public ObservableCollection<String> IgnoredActivities
{
get
{
if (_ignoredActivities == null)
{
// empty
_ignoredActivities = new ObservableCollection<String>() { "empty","stuff" };
}
return _ignoredActivities;
}
}
Anything more you need to know? What did I forget?
EDIT:
Maybe I should add that VisualStudio + ReSharper also show no underlines and compile errors. Not even warnings.
Sorry guys, the data was there all the time. The problem was in the visual details. The padding also got applied to a sub-container of the ListBox (or the items), therefore the items were not sitting at the top of the list. As I've but a height on the ListBox, the items always were below the visible height of the ListBox. Stange thing to debug. Thanks for your answers anyways!
1) You don't need to set the DataTemplate
2) Are you sure the DataContext of the view (aaaaa.xaml.cs) is the ViewModel (bbbbb.cs, contains IgnoredActivities)?
It should be like this:
aaaaa.xaml:
<ListBox ItemsSource="{Binding IgnoredActivities}" />
aaaaa.xaml.cs:
public partial class aaaaa : Window, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new bbbbb();
}
}
bbbbb.cs :
public class bbbbb : INotifyPropertyChanged
{
public bbbbb()
{
IgnoredActivities.Add("empty");
IgnoredActivities.Add("stuff");
}
private ObservableCollection<String> _ignoredActivities;
public ObservableCollection<String> IgnoredActivities
{
get
{
if (_ignoredActivities == null)
{
// empty
_ignoredActivities = new ObservableCollection<String>() { "empty","stuff" };
}
return _ignoredActivities;
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string _Prop)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(_Prop));
}
#endregion
}
}
With this statement you are binding to a property "IgnoredActivities" in a control named "model" of your Window.
ItemsSource="{Binding ElementName=model, Path=IgnoredActivities}"
When working with ViewModels, set them as the DataContext of your control:
<ListBox ItemsSource="{Binding IgnoredActivities}">
<ListBox.DataContext>
<local:MyViewModel/>
</ListBox.DataContext>
</ListBox>
A binding with no source specified ("{Binding PathToMyPropert}") defaults to the control's DataContext, which is inherited from parent controls. So in your case, you could set your ViewModel to your Window's DataContext so it will be available to all its children:
<Window>
<Window.DataContext>
<local:MyViewModel/>
</Window.DataContext>
<ListBox ItemsSource="{Binding IgnoredActivities}"/>
</Window>
Get rid of that ElementName binding property. It's used to bind to WPF controls. Simply set ItemsSource={Binding IgnoredActivities}
Try it as follows:
<ItemsControl ItemsSource="{Binding Path=IgnoredActivities}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<ListBox />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ListBoxItem Content="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Try also setting the DataContext for the control in code behind.
Xaml - <ListBox Name="myListBox" ItemsSource="{Binding IgnoredActivities}"/>
CodeBehind - myListBox.DataContext=this;