I try to understand (without success) why binding behaves differentially when source object is string[] and List<string>. I have two lists, their only difference is ItemsSource - in one case array in second List:
XAML code:
<StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Button Content="Modify items" Click="Button_Click"/>
<StackPanel Orientation="Horizontal">
<StackPanel.Resources>
<DataTemplate x:Key="ItemTemplate">
<TextBlock Text="{Binding}" FontSize="16"/>
</DataTemplate>
</StackPanel.Resources>
<ListView ItemsSource="{Binding ArrayElements}" ItemTemplate="{StaticResource ItemTemplate}" Width="100" Margin="20"/>
<ListView ItemsSource="{Binding ListElements}" ItemTemplate="{StaticResource ItemTemplate}" Width="100" Margin="20"/>
</StackPanel>
</StackPanel>
Code behind:
public sealed partial class MainPage : Page, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void RaiseProperty(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
public string[] ArrayElements { get; } = new string[] { "Standard", "Standard", "Standard" };
public List<string> ListElements { get; } = new List<string> { "Standard", "Standard", "Standard" };
public MainPage() { this.InitializeComponent(); this.DataContext = this; }
private void Button_Click(object sender, RoutedEventArgs e)
{
ArrayElements[1] = "Modified one";
ListElements[1] = "Modified one";
RaiseProperty(nameof(ArrayElements));
RaiseProperty(nameof(ListElements));
}
}
Once I click button, the first list (build upon array) gets refilled with new elements (seems like new ItemsSource), so I can see a little flicker and the change in second element.
The same time in the second list nothing changes - the reference to binding source doesn't change so we don't see the difference on screen (PropertyChange has no effect as no property has been changed) - that's clear.
So why the list where the ItemsSource is set to array behaves different?
Sample to reproduce the issue (though almost whole code is above)
It sounds weird to me but i never faced this Problem because i use ElementViewModel objects in an observable collection. Instead of changing the list-item / Array element i Change a property of the ElementViewModel and use the INotifyProperty Change there. It's not the actuall answer to you question but you can work around with this.
Related
I have a user control that I am using to populate a datagrid.
I would like the user to be able to add items by editing the empty row at the bottom. (This is why I am using a datagrid rather than an itemscontrol) However the datagrid does not realise that the last item is edited unless the user clicks the background of the control. I would like the new item to be added when the user makes changes on the properties that the control exposes.
XAML of the control:
<UserControl x:Class="ControlTest.MyControl"
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:ControlTest"
mc:Ignorable="d"
d:DesignHeight="50" d:DesignWidth="300"
DataContext="{Binding Path=., Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
>
<StackPanel Orientation="Vertical">
<TextBox Text="{Binding Path=p1, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
Width="300"
Height="30"
VerticalAlignment="Center"/>
<ComboBox ItemsSource="{Binding Path=DropDownValues,
RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType=local:MyControl}}"
SelectedItem="{Binding Path=p2, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
Height="30"/>
</StackPanel>
</UserControl>
cs:
public partial class MyControl : UserControl
{
private static readonly DependencyProperty DropDownValuesProperty =
DependencyProperty.Register(
"DropDownValues",
typeof(List<String>),
typeof(MyControl),
new FrameworkPropertyMetadata(new List<String>()
));
public List<String> DropDownValues
{
get
{
return (List<String>)GetValue(DropDownValuesProperty);
}
set
{
SetValue(DropDownValuesProperty, value);
}
}
public MyControl()
{
InitializeComponent();
}
}
DataGrid XAML
<DataGrid
AutoGenerateColumns="False"
ItemsSource="{Binding objs, Mode=TwoWay}"
HeadersVisibility="None"
Margin="0,0,0.4,0"
CanUserAddRows="True"
>
<DataGrid.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</DataGrid.ItemsPanel>
<DataGrid.Columns>
<DataGridTemplateColumn Width="300">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="local:Measure">
<local:MyControl
DataContext="{Binding ., Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
DropDownValues=
"{Binding DataContext.list, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
Width="300"
/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
Can I make this work, and/or is there a better way to do this?
I would like to suggest you a different way of doing that:
set CanUserAddRows=false on your DataGrid and then manually add rows to the ObservableCollection<Something> to which your DataGrid is bound to.
OR
If you are still interested in the approach that you follow:
In your xaml file:
<DataGrid x:Name="myDataGrid" CellEditEnding="DataGrid_CellEditEnding" .....>
<!--Some Code-->
</DataGrid>
Then in the Code-Behind:
private void DataGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
myDataGrid.CommitEdit();
}
If you don't understand anything then feel free to ask.
Update
If you are following the same approach:
In your DataGrid's Beginning edit event you can try:
private void DataGrid_BeginningEdit(object sender, DataGridBeginningEditEventArgs e)
{
if ((selectedRow as DataGridRow).Item.ToString() != "{NewItemPlaceholder}")
{
//Here you can add the code to add new item. I don't know how but you should figure out a way
}
}
Note: The code mentioned above is not tested.
I would also suggest you :
Not to use DataGrid. Instead use ListBox. Because, you are trying to add some data. At this time you never need sorting, searching and column-reordering fascilities. In such scenario, ListBox is useful as it is light-weight control than datagrid. I have a sample here: https://drive.google.com/open?id=0B5WyqSALui0bTXFGZWxQUWVRdkU
Is the problem that the UI is not being notified of changes to the objs collection? What I would do is try setting up whatever view model that contains objs to make objs an observable collection. I would also ensure that whatever objs is an observable collection of implements INotifyPropertyChanged and that properties p1 and p2 both fire OnPorpertyChanged when they are set.
public ObservableCollection<YourObject> objs
and
public class YourObject : INotifyPropertyChanged
{
protected void OnPropertyChanged(string Name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(Name));
}
}
private string _p1;
public string p1
{
get { return _p1; }
set
{
if (_p1 != value)
{
_p1 = value;
OnPropertyChanged("p1");
}
}
}
private string _p2;
public string p2
{
get { return _p2; }
set
{
if (_p2 != value)
{
_p2 = value;
OnPropertyChanged("p2");
}
}
}
}
I'm using MahApps for my first Wpf project, and more specifically tabControls. I tested it on a tiny test project. It worked well, until I tried to merge my code in my project.
Here's an example of my code behind :
public class Datas
{
public ObservableCollection<string> titles { get; set; }
public Datas()
{
init();
}
public void init()
{
titles = new ObservableCollection<string>()
{
"Title 1",
"Title 2",
"Title 3",
"Title 4"
};
}
}
public partial class Window1 : MetroWindow
{
private Datas datas;
public Window1()
{
init();
}
private void init()
{
datas = new Datas();
this.DataContext = this;
}
}
And my Xaml code :
<TabControl DataContext="{Binding Datas}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding titles}"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<TextBlock Text="Content" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
I've been searching for few days so far. I've found a topic about dataBinding to another class but it doesn't seem to work for me. Not sure if I can use both DataContexts in my Window1 class, even if I tried something like binding multiple controls to different dataContexts.
Do I really need something like that ? It seems to be bigger than what I need but I may be wrong. So, my problem is that I would like to have my tabs whose titles are those in my list, and it doesn't display anything (no error when running though).
Thanks for your help, and please be slow in your answers, I'm still new to Wpf :)
The Window:
<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">
<Grid>
<TabControl ItemsSource="{Binding Titles}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<TextBlock Text="Content" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</Window>
The Window's code behind:
public partial class MainWindow : Window
{
public MainWindow()
{
this.DataContext = new Datas();
this.InitializeComponent();
}
}
public class Datas
{
private ObservableCollection<String> titles;
public ObservableCollection<String> Titles
{
get
{
if (this.titles == null)
{
this.titles = new ObservableCollection<String>()
{
"Title 1",
"Title 2",
"Title 3",
"Title 4"
};
}
return this.titles;
}
}
}
Some advices:
Binding only works with public properties.
Do not expose an ObservableCollection if you are not going to modify it, use IList instead (it is lighter).
In WPF, at least for properties, lazy initialization is more natural than constructor initialization (see the Titles property).
Take a look at the .NET capitalization conventions, prefer using Pascal Casing for properties.
Set ItemsSource of TabControl and defind public property Datas in your Window's DataContext
<TabControl ItemsSource="{Binding Datas.titles}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</TabControl.ItemTemplate>
public partial class Window1 : MetroWindow
{
public Window1()
{
init();
}
private void init()
{
Datas = new Datas();
this.DataContext = this;
}
public Datas Datas{get;set;}
}
I have the following XAML:
...
<ListBox Name ="RoomsListBox" Height="100"
HorizontalAlignment="Left" Margin="12,41,0,0"
VerticalAlignment="Top" Width="120"></ListBox>
...
And the following C#-code:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
RoomsListBox.ItemsSource = new[] { new { Name = "First1" },
new { Name = "First2" } };
RoomsListBox.DisplayMemberPath = "Name";
}
The problem is that my ListBox have items but they are empty. Why I don't see "First1" and "First2" instead?
The issue here isn't with the bindings nor the ItemTemplate nor the change notification. It's the Anonymous Type you're using that's causing it. try using a class or struct for your items
public class Item
{
public string Name { get; set; }
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
RoomsListBox.ItemsSource = new[] {
new Item { Name = "First1" },
new Item { Name = "First2" }};
RoomsListBox.DisplayMemberPath = "Name";
}
your xaml stays the same, or you can define a DataTemplate for the ListBox items if you want. Note that you can't set both the ItemTemplate and DisplayMemberPath at the same time (one has to be null). Also, make sure that the class representing your items has to be public.
Hope this helps :)
Just a thought..
Have you tried settings the DisplayMemberPath property in XAML? There might be an issue with the order of calls.
You have to set DisplayMemberPath property on your ListBox to Name.
Moving forward you might want to consider creating a DataTemplate for your items to have more control:
<ListBox x:Name ="RoomsListBox" Height="100"
HorizontalAlignment="Left" Margin="12,41,0,0"
VerticalAlignment="Top" Width="120">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Name}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
See this tutorial for more info: http://weblogs.asp.net/scottgu/pages/silverlight-tutorial-part-5-using-the-listbox-and-databinding-to-display-list-data.aspx
I would prefer that you define your binding in your xaml and for example in your Code-Behind you define a property for the items of your listbox.
Example: (xaml)
<ListBox Name ="RoomsListBox"
ItemsSource="{Binding MyItems}"
Height="100"
HorizontalAlignment="Left"
Margin="12,41,0,0"
VerticalAlignment="Top"
Width="120" />
Example: (C# in your code-behind)
//...
private ObservableCollection<string> _myItems;
public ObservableCollection<String> MyItems
{
get
{
return _myItems ?? (_myItems = new ObservableCollection<string> { "FirstItem", "SecondItem"});
}
set
{
_myItems = value;
}
}
Like ChrisF said you could use the INotifiyPropertyChanged Interface, there you would raise the PropertyChanged event in the setter of your property.
See --> http://msdn.microsoft.com/en-us/library/system.componentmodel.inotifypropertychanged.aspx
I have MVVM master /details like this:
<Window.Resources>
<DataTemplate DataType="{x:Type model:EveryDay}">
<views:EveryDayView/>
</DataTemplate>
<DataTemplate DataType="{x:Type model:EveryMonth}">
<views:EveryMonthView/>
</DataTemplate>
</Window.Resources>
<Grid>
<ListBox Margin="12,24,0,35" Name="schedules"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Path=Elements}"
SelectedItem="{Binding Path=CurrentElement}"
DisplayMemberPath="Name" HorizontalAlignment="Left" Width="120"/>
<ContentControl Margin="168,86,32,35" Name="contentControl1"
Content="{Binding Path=CurrentElement.Schedule}" />
<ComboBox Height="23" Margin="188,24,51,0" Name="comboBox1"
VerticalAlignment="Top"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Path=Schedules}"
SelectedItem="{Binding Path=CurrentElement.Schedule}"
DisplayMemberPath="Name"
SelectedValuePath="ID"
SelectedValue="{Binding Path=CurrentElement.Schedule.ID}"/>
</Grid>
This Window has DataContext class:
public class MainViewModel : INotifyPropertyChanged {
public MainViewModel() {
elements.Add(new Element("first", new EveryDay("First EveryDay object")));
elements.Add(new Element("second", new EveryMonth("Every Month object")));
elements.Add(new Element("third", new EveryDay("Second EveryDay object")));
schedules.Add(new EveryDay());
schedules.Add(new EveryMonth());
}
private ObservableCollection<ScheduleBase> _schedules = new
ObservableCollection<ScheduleBase>();
public ObservableCollection<ScheduleBase> Schedules {
get {
return _schedules;
}
set {
schedules = value;
this.OnPropertyChanged("Schedules");
}
}
private Element _currentElement = null;
public Element CurrentElement {
get {
return this._currentElement;
}
set {
this._currentElement = value;
this.OnPropertyChanged("CurrentElement");
}
}
private ObservableCollection<Element> _elements = new
ObservableCollection<Element>();
public ObservableCollection<Element> Elements {
get {
return _elements;
}
set {
elements = value;
this.OnPropertyChanged("Elements");
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName) {
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) {
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
One of Views:
<UserControl x:Class="Views.EveryDayView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid >
<GroupBox Header="Every Day Data" Name="groupBox1" VerticalAlignment="Top">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<TextBox Name="textBox2" Text="{Binding Path=AnyDayData}" />
</Grid>
</GroupBox>
</Grid>
My SelectedItem in ComboBox doesn't works correctly. Are there any visible errors in my code?
What I usually do is bind the items of an ItemsControl to an ICollectionView (usually ListCollectionView) instead of directly to a collection; I think that's what the ItemsControl does by default anyway (creates a default ICollectionView), but I might be wrong.
Anyway, this allows you to work with the CurrentItem property of the ICollectionView, which is automatically synchronized with the selected item in an ItemsControl (if the IsSynchronizedWithCurrentItem property of the control is true or null/default). Then, when you need the current item in the ViewModel, you can use that instead. You can also set the selected item by using the MoveCurrentTo... methods on the ICollectionView.
But as I re-read the question I realize you may have another problem altogether; you have a collection of 'default' items and need a way to match them to specific instances. It would however be a bad idea to override the equality operators of the objects to consider them always equal if they are of the same type, since that has the potential to make other code very confusing. I would consider extracting the type information into an enum, and put a read-only property on each object returning one of the enum values. Then you can bind the items to a collection of the enum values, and the selected item to the enum property of each object.
Let me know if you need an example, I may have made a mess of the explanation :)
I'm databinding a listbox to an object that contains two array of strings. Each listbox item is set to a data template made up of a textbox and a combo box. The first string array is bound to the list, and the second string array is bound to the combo box. Well, at least that's I'm trying to achieve. The problem is that I can't figure out the binding syntax to set the second array to the combo box. Here's what I have:
The first thing is my class with my two string arrays. Pretty straightforward. Please note that the string array content is there for simplicity.
public class JobAssignments
{
public JobAssignments()
{
m_people.Add("John");
m_people.Add("Bill");
m_people.Add("Frank");
m_people.Add("Steve");
m_jobs.Add("Architect");
m_jobs.Add("Teacher");
m_jobs.Add("Carpenter");
m_jobs.Add("Plumber");
}
private List<string> m_people = new List<string>();
public List<string> People { get { return m_people; } set { m_people = value; } }
private List<string> m_jobs = new List<string>();
public List<string> Jobs { get { return m_jobs; } set { m_jobs = value; } }
};
In code, I set an instance of this class as the datacontext of this listbox:
<ListBox x:Name="listBox"
Grid.Row="0"
HorizontalContentAlignment="Stretch"
DataContext="{Binding}"
ItemsSource="{Binding People}"
ItemTemplate="{StaticResource JobAssignmentDataTemplate}">
</ListBox>
With a data template that looks like this:
<DataTemplate x:Key="JobAssignmentDataTemplate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="{Binding}"/>
<ComboBox Grid.Column="2"
SelectedIndex="0"
ItemsSource="{Binding Jobs ???? }"/>
</Grid>
</DataTemplate>
What I usually get out my experiments is a list box of People, but the combo box of each list item is empty.
I can get it to work if I use
ItemsSource="{Binding ElementName=listBox, Path=DataContext.Jobs }"/>
but I don't want to use ElementName as it hardcodes the source of the array a specific listbox which I'd like to avoid.
Trying something like
ItemsSource="{Binding RelativeSource={RelativeSource Self}, Path=Parent.Jobs}"/>
Doesn't seem to work either as it's looking for Jobs inside the Grid.
A little push in the right direction would help greatly.
Thanks!
I had roughly the same problem as you except I had a listbox within a tabcontrol. I was able to solve it by using getting the tab control that contained the listbox. Sample code as follows:
<TabControl ItemsSource="{Binding Groups, Mode=TwoWay}" SelectedItem="{Binding SelectedGroup, Mode=TwoWay}">
<TabControl.ContentTemplate>
<DataTemplate>
<ListBox ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=TabControl, AncestorLevel=1}, Path=DataContext.ItemsInGroup, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ItemGroup}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
When you are at DataTemplate that represents just one People your JobAssignments instance is not there. Try the below
ItemsSource="{Binding ElementName= listBox, Path=Jobs}"/>
But I dont recommend the above step. You need to refactor your ViewModel logic. I guess 'Jobs' can be a static instance so that you can do a x:Static binding
ItemsSource="{Binding Source={x:Static JobAssignments}, Path=Jobs}"
You probably need to modify your object structure to work like this:
public class Person
{
public string Name;
public IEnumerable<string> Jobs;
}
public class JobAssignments
{
public JobAssignments()
{
Jobs.Add("Architect");
...
People.Add(new Person() { Name = "Bob", Jobs = Jobs });
...
}
private List<Person> m_people = new List<Person>();
public List<Person> People { get { return m_people; } }
private List<string> m_jobs = new List<string>();
public List<string> Jobs { get { return m_jobs; } }
}
Then, you can remove the question marks, and things should work.