I want to change the binding path SomeProperty to another at runtime. And the PropertyName can be any string ,so I can't set it before it running. how to do this?
I tried "FindName" method to find tb1 but it's not work.
Partical code :
<ListBox>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Name="tb1" Text="{Binding Path=SomeProperty}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Maybe you're simply looking for "DisplayMemberPath"?
<ListBox ItemsSource="{Binding Items}" DisplayMemberPath="SomeProperty" />
You cannot mix it with DataTemplates but in your example you didn't really need it.
You can create a normal binding for this property:
<StackPanel>
<ListBox ItemsSource="{Binding Items}"
DisplayMemberPath="{Binding ElementName=DisplayPathText, Path=Text}" />
<TextBox Name="DisplayPathText" Text="SomeProperty" />
</StackPanel>
Due to the lack of context, let's try to establish one.
Assuming your ListBox is binding to a Person list with FirstName and LastName property, initial binding would be FirstName and you want to change it to LastName during runtime when clicking on a button.
This is how you can achieve it.
View
<ListBox Name="LstBx" ItemsSource="{Binding PersonList}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Name="tb1" Text="{Binding Path=FirstName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Click="Button_Click" Width="100" Height="20" Content="Change Binding"/>
CodeBehind
public List<Person> PersonList { get; set; } = new List<Person>
{
new Person { FirstName = "ABC", LastName = "123" },
new Person { FirstName = "DEF", LastName = "456" },
new Person { FirstName = "GHI", LastName = "789" }
};
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
foreach (var person in PersonList)
{
var listBoxItem = LstBx.ItemContainerGenerator.ContainerFromItem(person);
var contentPresenter = FindVisualChild<ContentPresenter>(listBoxItem);
var target = contentPresenter.ContentTemplate.FindName("tb1", contentPresenter) as TextBox;
if (target != null)
{
var binding = new Binding
{
// Remember each ListBoxItem's DataContext is the individual item
// in the list, not the list itself.
Source = person,
Path = new PropertyPath(nameof(Person.LastName)),
// Depends on what you need.
//Mode = BindingMode.TwoWay,
//UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
};
BindingOperations.SetBinding(target, TextBox.TextProperty, binding);
}
}
}
// Available from MSDN
private Child FindVisualChild<Child>(DependencyObject obj) where Child : DependencyObject
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
var child = VisualTreeHelper.GetChild(obj, i);
if (child != null && child is Child)
{
return (Child)child;
}
else
{
var childOfChild = FindVisualChild<Child>(child);
if (childOfChild != null) { return childOfChild; }
}
}
return null;
}
Related
I have a List of Lists and display it with nested ListBoxes:
MainWindow.xaml.cs
using System.Collections.Generic;
namespace WPF_Sandbox
{
public partial class MainWindow
{
public IEnumerable<IEnumerable<string>> ListOfStringLists { get; set; } = new[] { new[] { "a", "b" }, new[] { "c", "d" } };
public MainWindow()
{
InitializeComponent();
DoSomethingButton.Click += (sender, e) =>
{
// do something with all selected items
};
}
}
}
MainWindow.xaml
<Window x:Class="WPF_Sandbox.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
x:Name="ThisControl">
<StackPanel>
<ListBox ItemsSource="{Binding ListOfStringLists, ElementName=ThisControl}">
<ListBox.ItemTemplate>
<ItemContainerTemplate>
<ListBox ItemsSource="{Binding}" SelectionMode="Multiple">
<ListBox.ItemTemplate>
<ItemContainerTemplate>
<TextBlock Text="{Binding}" />
</ItemContainerTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ItemContainerTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Name="DoSomethingButton" Content="DoSomething" />
</StackPanel>
</Window>
How can I get all selected items across all ListBoxes?
I found a few solutions getting one selected item but could not figure out how to do applie those in my scenario.
I have an idea on how to do this by wrapping the string arrays but I would prefer not doing this.
I would just add an event handler to the inner ListBox like so if not doing things the MVVM way:
<ListBox ItemsSource="{Binding}" SelectionMode="Multiple" SelectionChanged="ListBox_SelectionChanged">
Then in your code behind implement the ListBox_SelectionChanged like so:
public List<string> FlatStringList = new List<string>();
private void ListBox_SelectionChanged(object sender,System.Windows.Controls.SelectionChangedEventArgs e)
{
FlatStringList.AddRange(e.AddedItems.Cast<string>());
foreach(string s in e.RemovedItems)
{
FlatStringList.Remove(s);
}
}
This is assuming you don't mind storing the selected strings in a flat list. Then you could implement your DoSomething button click event handler to do something with the FlatStringList.
Hope that helps.
The easiest way would be to iterate through the items in the ListBoxes:
private void DoSomethingButton_Click(object sender, RoutedEventArgs e)
{
List<string> selectedStrings = new List<string>();
foreach (IEnumerable<string> array in outerListBox.Items.OfType<IEnumerable<string>>())
{
ListBoxItem lbi = outerListBox.ItemContainerGenerator.ContainerFromItem(array) as ListBoxItem;
if (lbi != null)
{
ListBox innerListBox = GetChildOfType<ListBox>(lbi);
if (innerListBox != null)
{
foreach (string selectedString in innerListBox.SelectedItems.OfType<string>())
selectedStrings.Add(selectedString);
}
}
}
}
private static T GetChildOfType<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj == null)
return null;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
var result = (child as T) ?? GetChildOfType<T>(child);
if (result != null)
return result;
}
return null;
}
Note that the ListBoxItem may be virtualized away if you have a lot of inner IEnumerable<string>. You will then have to force the generation of the containers or disable UI virtualization:
WPF ListView virtualization. How to disable ListView virtualization?
This may affect the performance negatively so if this is an issue you should probably consider binding to an IEnumerable<YourType> and bind the SelectedItems property of the inner ListBox to a property of a YourType using a behaviour.
Since the SelectedItems property of a ListBox is read-only you can't bind to it directly: https://blog.magnusmontin.net/2014/01/30/wpf-using-behaviours-to-bind-to-readonly-properties-in-mvvm/.
Why don't you create a wrapper (as you said):
public class MyString : INotifyPropertyChanged
{
public MyString(string value) { Value = value; }
string _value;
public string Value { get { return _value; } set { _value = value; RaisePropertyChanged("Value"); } }
bool _isSelected;
public bool IsSelected { get { return _isSelected; } set { _isSelected = value; RaisePropertyChanged("IsSelected"); } }
public event PropertyChangedEventHandler PropertyChanged;
void RaisePropertyChanged(string propname)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
}
}
Bind the IsSelected property of the ListBoxItems:
<StackPanel>
<ListBox ItemsSource="{Binding ListOfStringLists, ElementName=ThisControl}">
<ListBox.ItemTemplate>
<ItemContainerTemplate>
<ListBox ItemsSource="{Binding}" SelectionMode="Multiple">
<ListBox.ItemTemplate>
<ItemContainerTemplate>
<TextBlock Text="{Binding Value}" />
</ItemContainerTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</ItemContainerTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Name="DoSomethingButton" Content="DoSomething" />
</StackPanel>
and you are already done:
public IEnumerable<IEnumerable<MyString>> ListOfStringLists { get; set; } = new[] { new[] { new MyString("a"), new MyString("b") { IsSelected = true } }, new[] { new MyString("c"), new MyString("d") } };
public MainWindow()
{
this.InitializeComponent();
DoSomethingButton.Click += (sender, e) =>
{
foreach (var i in ListOfStringLists)
foreach (var j in i)
{
if (j.IsSelected)
{
// ....
}
}
};
}
I've got this code. I need to have access to the ScheduleList from my c# code. But it's inaccessible. I can get access to SchedulePivot only.
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" Margin="0,0,0,50">
<Pivot x:Name="SchedulePivot" Margin="10,10,10,0" Title="Pivot" VerticalAlignment="Top">
<Pivot.ItemTemplate>
<DataTemplate>
<ListBox x:Name="ScheduleList" Margin="0,0,0,17" Width="Auto">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Height="52" Width="auto">
Searching on StackOverflow I have found this code:
private DependencyObject FindChildControl<T>(DependencyObject control, string ctrlName)
{
int childNumber = VisualTreeHelper.GetChildrenCount(control);
for (int i = 0; i < childNumber; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(control, i);
FrameworkElement fe = child as FrameworkElement;
// Not a framework element or is null
if (fe == null) return null;
if (child is T && fe.Name == ctrlName)
{
// Found the control so return
return child;
}
else
{
// Not found it - search children
DependencyObject nextLevel = FindChildControl<T>(child, ctrlName);
if (nextLevel != null)
return nextLevel;
}
}
return null;
}
I use this line to get the child:
ListBox listCont = FindChildControl<ListBox>(this, "ScheduleList") as ListBox;
Also I tried doing like this:
ListBox listCont = FindChildControl<ListBox>(SchedulePivot, "ScheduleList") as ListBox;
than I do this:
listCont.Items.Add(items);
And get the exeption as listCont=null. What's wrong I'm doing?
I have tested your code, both of the following code work well in my side and I can get the correct result:
ListBox listCont = FindChildControl<ListBox>(this, "ScheduleList") as ListBox;
ListBox listCont = FindChildControl<ListBox>(SchedulePivot, "ScheduleList") as ListBox;
If we want to access the control by using the VisualTreeHelper, we should make sure that we have not called the above code inside the constructor of the MainPage, or we will get the null result as below. Because the control does not been initialized completely:
In order to get the correct result, we need to call the above code inside the MainPage.Loaded event or Button click event to make sure that control has been initialized completely, after that it should work fine.
The following is my sample, please try to refer to:
In the MainPage.xaml:
<Pivot x:Name="SchedulePivot" ItemsSource="{Binding PivotTestlist}" Margin="10,10,10,0" Title="Pivot" VerticalAlignment="Top">
<Pivot.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding header}"></TextBlock>
</DataTemplate>
</Pivot.HeaderTemplate>
<Pivot.ItemTemplate>
<DataTemplate>
<ListBox x:Name="ScheduleList" Margin="0,0,0,17" Width="Auto" ItemsSource="{Binding ListBoxTestlist}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Height="52" Width="auto">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding id}"></TextBlock>
<TextBlock Text="{Binding name}"></TextBlock>
</StackPanel>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DataTemplate>
</Pivot.ItemTemplate>
</Pivot>
<Button Click="Button_Click" Content="Button"></Button>
In the MainPage.xaml.cs:
public class ListBoxTest
{
public string name { get; set; }
public string id { get; set; }
}
public class PivotTest
{
public List<ListBoxTest> ListBoxTestlist { get; set; }
public string header { get; set; }
}
public sealed partial class MainPage : Page
{
public List<PivotTest> PivotTestlist { get; set; }
public MainPage()
{
this.InitializeComponent();
PivotTestlist = new List<PivotTest>();
PivotTest PivotTest1 = new PivotTest();
PivotTest1.ListBoxTestlist = new List<ListBoxTest>();
PivotTest1.ListBoxTestlist.Add(new ListBoxTest() { name = "name1", id = "id1" });
PivotTest1.ListBoxTestlist.Add(new ListBoxTest() { name = "name2", id = "id2" });
PivotTest1.header = "header1";
PivotTestlist.Add(PivotTest1);
PivotTest PivotTest2 = new PivotTest();
PivotTest2.ListBoxTestlist = new List<ListBoxTest>();
PivotTest2.ListBoxTestlist.Add(new ListBoxTest() { name = "name11", id = "id11" });
PivotTest2.ListBoxTestlist.Add(new ListBoxTest() { name = "name22", id = "id22" });
PivotTest2.header = "header2";
PivotTestlist.Add(PivotTest2);
this.DataContext = this;
}
private DependencyObject FindChildControl<T>(DependencyObject control, string ctrlName)
{
int childNumber = VisualTreeHelper.GetChildrenCount(control);
for (int i = 0; i < childNumber; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(control, i);
FrameworkElement fe = child as FrameworkElement;
// Not a framework element or is null
if (fe == null) return null;
if (child is T && fe.Name == ctrlName)
{
// Found the control so return
return child;
}
else
{
// Not found it - search children
DependencyObject nextLevel = FindChildControl<T>(child, ctrlName);
if (nextLevel != null)
return nextLevel;
}
}
return null;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
ListBox listCont = FindChildControl<ListBox>(SchedulePivot, "ScheduleList") as ListBox;
int count = listCont.Items.Count;
}
}
The result:
Declare the Pivot in a storyboard and use the x:Key instead of x:Name.
e.g.
<StoryBoard>
<Pivot x:key="nameIt"/>
</StoryBoard>
private void AccesPivot ()
{ //now you can acces your pivot
}
I am new-bee at WPF, i am trying to populate my combox control which is there within my listbox
XAML :
<Window.Resources>
<DataTemplate x:Key="UserTemplate" >
<StackPanel Orientation="Horizontal" >
<ComboBox Name="rule" ItemsSource="{Binding}" DisplayMemberPath="DataContext.RuleType" Width="85" Height="20"
SelectedValuePath="DataContext.RuleType" SelectedValue="{Binding Path=DataContext.RuleType}"/>
<TextBlock Text="{Binding Path= Name1}" Width="85" Margin="5,5,5,5"></TextBlock>
<Button Content="Delete" Click="cmdDeleteUser_Clicked" Margin="5,5,5,5" />
<Button Content="Add" Click="cmdAddUser_Clicked" Margin="5,5,5,5" />
</StackPanel>
</DataTemplate>
</Window.Resources>
<Grid>
<ListBox Name="lbUsers" ItemsSource="{Binding }" ItemTemplate="{StaticResource UserTemplate}"/>
</Grid>
CODE BEHIND:
public ObservableCollection<User> Users;
ObservableCollection<Listdata> listeddata;
ObservableCollection<Records> Record;
public MainWindow()
{
InitializeComponent();
Users = new ObservableCollection<User>() {
new User() { Name = "", Age = "" },
};
DataboundListbox.Records record = new Records();
RuleType = record.record_Rule();
lbUsers.DataContext = Users;
}
private string _Name;
public string Name1
{
get { return _Name; }
set
{
if (value != _Name)
{
_Name = "John";
NotifyPropertyChanged("Name");
}
}
}
private List<string> _RuleType;
public List<string> RuleType
{
get { return _RuleType; }
set
{
if (value != _RuleType)
{
_RuleType = value;
NotifyPropertyChanged("RuleType");
}
}
}
private void NotifyPropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
private void cmdDeleteUser_Clicked(object sender, RoutedEventArgs e)
{
Button cmd = (Button)sender;
if (cmd.DataContext is User)
{
User deleteme = (User)cmd.DataContext;
Users.Remove(deleteme);
}
}
private void cmdAddUser_Clicked(object sender, RoutedEventArgs e)
{
Button cmd = (Button)sender;
if (cmd.DataContext is User)
{
var addedUser = new User() { Name = "", Age = "" };
Users.Add(addedUser);
}
}
private List<string> _prp;
public List<string> prp
{
get { return _prp; }
set
{
if (value != _prp)
{
_RuleType = value;
NotifyPropertyChanged("prp");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
Before I can answer your question there are some confusions that should be cleared up.
If User has already a member named Name then what's Name1 in parent class for?
If RuleType is a list, how come it's set as the SelectedValue of your ComboBox, Shouldn't it be ComboBox.itemsSource instead? If it should, then where is the property defined to keep the ComboBox.SelectedValue?
How come there is an Add button inside the UserTemplate? Delete button is ok but i think Add belongs outside of the ListBox.
If i understand your issue correctly, then this is the solution I can think of.
Fisrt: User needs a property like SelectedRule to keep Combobox.SelectedItem:
public class User : INotifyPropertyChanged
{
// implementation of INotifyPropertyChanged
string _name;
public string Name
{
get
{
return _name;
}
set
{
_name = value;
NotifyPropertyChanged("Name");
}
}
int _age;
public int Age
{
get
{
return _age;
}
set
{
_age = value;
NotifyPropertyChanged("Age");
}
}
string _selectedRule;
public string SelectedRule
{
get
{
return _selectedRule;
}
set
{
_selectedRule = value;
NotifyPropertyChanged("SelectedRule");
}
}
}
Second: Your DataTemplate should change like this:
<Window.Resources>
<DataTemplate x:Key="UserTemplate" >
<StackPanel Orientation="Horizontal" >
<ComboBox Name="rule" ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=RuleType}" DisplayMemberPath="." Width="85" Height="20"
SelectedItem="{Binding SelectedRule}"/>
<TextBlock Text="{Binding Path= Name}" Width="85" Margin="5,5,5,5"></TextBlock>
<Button Content="Delete" Click="cmdDeleteUser_Clicked" Margin="5,5,5,5" />
</StackPanel>
</DataTemplate>
</Window.Resources>
Finally the ListBox part changes as below:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListBox Grid.Row="0" Name="lbUsers" ItemsSource="{Binding}" ItemTemplate="{StaticResource UserTemplate}"/>
<Button Grid.Row="1" Content="Add" Click="cmdAddUser_Clicked" Margin="5,5,5,5" />
</Grid>
If you're gonna bring Add button out like the above code, then you should remove if (cmd.DataContext is User) from cmdAddUser_Clicked method.
Problem :
The main problem is on this two line:
{Binding Path=DataContext.RuleType}
{Binding Path= Name1}
Since you already declare your dataContext, DataContext.RuleType will causes the compiler to search for yourdatacontext.DataContext.RuleType which is obviously not the thing you want.
lbUsers.DataContext = Users;
Your data context is a collection of User class and does not contain Name1. Thus Binding Path=Name1 will return "property not found" error
Solution
In WPF, MVVM ( model view viewmodel) pattern is highly encouraged. One of its main feature is it seperate GUI logic from Business Logic, making the code cleaner and easier to maintain.
Step 1: Create a ViewModel
public class UserViewModel:INotifyPropertyChanged
{
private string name;
private string age;
private string rule;
private List<string> ruleType;
public String Name
{
get { return name; }
set { name = value; NotifyPropertyChanged("Name"); }
}
public String Age
{
get { return age; }
set { age = value; NotifyPropertyChanged("Age"); }
}
public String Rule
{
get { return rule; }
set { rule = value; NotifyPropertyChanged("Rule"); }
}
public List<string> RuleType
{
get { return ruleType; }
set { ruleType = value; NotifyPropertyChanged("RuleType"); }
}
public UserViewModel()
{
name = "name";
age = "";
ruleType = new List<string>();
}
#region NotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#endregion
}
}
Step 2 : Link your data context to the viewmodel
public MainWindow()
{
InitializeComponent();
Users = new ObservableCollection<UserViewModel>();
//setup your data here
//example:
UserViewModel userViewModel = new UserViewModel();
//populate your combobox here
userViewModel.RuleType.Add("rule1")
userViewModel.RuleType.Add("rule2");
userViewModel.RuleType.Add("rule3");
Users.Add(new UserViewModel());
lbUsers.DataContext = Users ;
}
Step 3 : Update your xaml
<Window.Resources>
<DataTemplate x:Key="UserTemplate" >
<StackPanel Orientation="Horizontal" >
<ComboBox Name="rule" ItemsSource="{Binding RuleType}" Width="85" Height="20"
SelectedValue="{Binding Rule}"/>
<TextBlock Text="{Binding Path= Name}" Width="85" Margin="5,5,5,5"></TextBlock>
<Button Content="Delete" Click="cmdDeleteUser_Clicked" Margin="5,5,5,5" />
<Button Content="Add" Click="cmdAddUser_Clicked" Margin="5,5,5,5" />
</StackPanel>
</DataTemplate>
</Window.Resources>
When i am typing, bahman already post a quite detailed answer.So i stopped here. If you require any explaination or solution from me just asked will do.
In future if you suspect any error regarding binding, you can search your output window.
If you see your output window you possibly will found this
System.Windows.Data Error: 40 : BindingExpression path error: 'DataContext' property not found on 'object' ''User' (HashCode=9080996)'. BindingExpression:Path=DataContext.RuleType; DataItem='User' (HashCode=9080996); target element is 'ComboBox' (Name=''); target property is 'SelectedValue' (type 'Object')
System.Windows.Data Error: 40 : BindingExpression path error: 'Name1' property not found on 'object' ''User' (HashCode=9080996)'. BindingExpression:Path=Name1; DataItem='User' (HashCode=9080996); target element is 'TextBlock' (Name=''); target property is 'Text' (type 'String')
I created a new TextBlock class which has ItemsSource property and translates that ItemsSource into "Run" object:
public class MultiTypeDynamicTextBlock : TextBlock
{
public interface ISection
{
Inline GetDisplayElement();
}
public class TextOption : ISection
{
private Run mText;
public TextOption(string aText)
{
mText = new Run();
mText.Text = aText.Replace("\\n", "\n");
}
public Inline GetDisplayElement()
{
return mText;
}
}
public class LineBreakOption : ISection
{
public Inline GetDisplayElement()
{
return new LineBreak();
}
public ISection Clone()
{
return new LineBreakOption();
}
}
public class ImageOption : ISection
{
private InlineUIContainer mContainer;
public ImageOption(string aDisplay)
{
Image lImage;
lImage = new Image();
lImage.Source = new BitmapImage(new Uri(Environment.CurrentDirectory + aDisplay));
lImage.Height = 15;
lImage.Width = 15;
mContainer = new InlineUIContainer(lImage);
}
public Inline GetDisplayElement()
{
return mContainer;
}
}
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(ObservableCollection<ISection>), typeof(MultiTypeDynamicTextBlock),
new UIPropertyMetadata(new ObservableCollection<ISection>(),
new PropertyChangedCallback(SetContent)));
public ObservableCollection<ISection> ItemsSource
{
get
{
return GetValue(ItemsSourceProperty) as ObservableCollection<ISection>;
}
set
{
if (ItemsSource != null)
ItemsSource.CollectionChanged -= CollectionChanged;
SetValue(ItemsSourceProperty, value);
SetContent();
ItemsSource.CollectionChanged += CollectionChanged;
}
}
private void CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
SetContent();
}
private static void SetContent(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DependencyObject lParent = d;
MultiTypeDynamicTextBlock lPanel = lParent as MultiTypeDynamicTextBlock;
if (lPanel != null)
{
lPanel.ItemsSource = e.NewValue as ObservableCollection<ISection>;
}
}
private void SetContent()
{
if (ItemsSource != null)
{
Inlines.Clear();
foreach (ISection lCurr in ItemsSource)
{
Inlines.Add(lCurr.GetDisplayElement());
}
}
}
If I Bind the ItemsSource directly to the DataContext, it works.
But if I bind it to an object that changes at runtime (such as SelectedItem on a ListBox) it doesn't update the text when a new item is selected.
<StackPanel>
<ListBox x:Name="TheList" ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Title}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel DataContext="{Binding ElementName=TheList, Path=SelectedItem}">
<TextBlock Text="{Binding Title}" FontSize="20"/>
<local:MultiTypeDynamicTextBlock ItemsSource="{Binding Items}"/>
</StackPanel>
</StackPanel>
Any reason why?
In your example, does the SelectedItem has two properties Title and Items? Or is Items a property in your viewmodel? If the answer is the latter, than you can find a solution below.
I don't entirely understand what you mean, but I'll give it a try.
If you mean that the ItemsSource on your custom control isn't set, than you have to point XAML into the right direction.
Below you can find a solution, if this is what you want to achieve.
What I did is pointing the compiler to the right source with this line of code:
ItemsSource="{Binding DataContext.Items, RelativeSource={RelativeSource AncestorType=Window}}"
Here you say that the compiler can find the Binding property in the DataContext of the Window (or any control where you can find the property).
<StackPanel>
<ListBox x:Name="TheList" ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Title}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel DataContext="{Binding ElementName=TheList, Path=SelectedItem}">
<TextBlock Text="{Binding Title}" FontSize="20"/>
<local:MultiTypeDynamicTextBlock ItemsSource="{Binding DataContext.Items, RelativeSource={RelativeSource AncestorType=Window}}"/>
</StackPanel>
</StackPanel>
Hopefully this helped.
EDIT
The title property will changes when I select another one from the ListBox.
If Items is set to a new ObservableCollection, do you call the OnPropertyChanged event for Items when the SelectedItem changes?
OnPropertyChanged("Items");
Thank you for your help.
I managed to fix this by updating the MultiTypeDynamicTextBlock as follows:
public class MultiTypeDynamicTextBlock : TextBlock
{
public interface ISection
{
Inline GetDisplayElement();
ISection Clone();
}
public class TextOption : ISection
{
private Run mText;
public TextOption(string aText)
{
mText = new Run();
mText.Text = aText.Replace("\\n", "\n");
}
public Inline GetDisplayElement()
{
return mText;
}
public ISection Clone()
{
return new TextOption(mText.Text);
}
}
public class LineBreakOption : ISection
{
public Inline GetDisplayElement()
{
return new LineBreak();
}
public ISection Clone()
{
return new LineBreakOption();
}
}
public class SectionList
{
private ObservableCollection<ISection> mList;
public Action CollectionChanged;
public ObservableCollection<ISection> Items
{
get
{
ObservableCollection<ISection> lRet = new ObservableCollection<ISection>();
foreach (ISection lCurr in mList)
{
lRet.Add(lCurr.Clone());
}
return lRet;
}
}
public int Count { get { return mList.Count; } }
public SectionList()
{
mList = new ObservableCollection<ISection>();
}
public void Add(ISection aValue)
{
mList.Add(aValue);
}
public SectionList Clone()
{
SectionList lRet = new SectionList();
lRet.mList = Items;
return lRet;
}
}
public MultiTypeDynamicTextBlock()
{
}
public static readonly DependencyProperty ItemsCollectionProperty =
DependencyProperty.Register("ItemsCollection", typeof(SectionList), typeof(MultiTypeDynamicTextBlock),
new UIPropertyMetadata((PropertyChangedCallback)((sender, args) =>
{
MultiTypeDynamicTextBlock textBlock = sender as MultiTypeDynamicTextBlock;
SectionList inlines = args.NewValue as SectionList;
if (textBlock != null)
{
if ((inlines != null) && (inlines.Count > 0))
{
textBlock.ItemsCollection.CollectionChanged += textBlock.ResetInlines;
textBlock.Inlines.Clear();
foreach (ISection lCurr in textBlock.ItemsCollection.Items)
{
textBlock.Inlines.Add(lCurr.GetDisplayElement());
}
}
else
{
inlines = new SectionList();
inlines.Add(new TextOption("No value set"));
textBlock.ItemsCollection = inlines;
}
}
})));
public SectionList ItemsCollection
{
get
{
return (SectionList)GetValue(ItemsCollectionProperty);
}
set
{
SectionList lTemp;
if (value == null)
{
lTemp = new SectionList();
lTemp.Add(new TextOption("No value set for property"));
}
else
{
lTemp = value;
}
SetValue(ItemsCollectionProperty, lTemp);
}
}
private void ResetInlines()
{
Inlines.Clear();
foreach (ISection lCurr in ItemsCollection.Items)
{
Inlines.Add(lCurr.GetDisplayElement());
}
}
}
And I update the fields that were Binded to be of type MultiTypeDynamicTextBlock.SectionList
As long as I am using a copy (Clone) it is working, for some reason when I don't clone it removes the value from the display in the list, if someone knows why I would love to learn but I managed to go around it.
the XAML of the window is:
<StackPanel>
<ListBox x:Name="TheList" ItemsSource="{Binding GeneralItems}" SelectedItem="{Binding SelectedItem}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Title}" FontSize="20"/>
<local:MultiTypeDynamicTextBlock ItemsCollection="{Binding Items}" Margin="20,0,0,0"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel DataContext="{Binding GeneralItems, Path=SelectedItem}">
<TextBlock Text="{Binding Title}" FontSize="20"/>
<local:MultiTypeDynamicTextBlock DataContext="{Binding Items}" ItemsCollection="{Binding}" Margin="20,0,0,0"/>
</StackPanel>
</StackPanel>
Today I am stucked in very basic concept again. What is the mistake I am doing.
I have XAML like
<ComboBox ItemsSource="{Binding MyItems}" Height="40" Width="200" SelectedIndex="0"
SelectedItem="{Binding MySelectedItem}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Text="{Binding MySelectedItem.Name}"/>
<Button Content="Test" Grid.Row="1" Click="Button_Click_1"/>
My ModelView looks like
public class MainViewModel : DependencyObject,INotifyPropertyChanged
{
private MyModel mySelectedItem;
public MyModel MySelectedItem
{
get
{
return mySelectedItem;
}
set
{
if (value != mySelectedItem)
{
mySelectedItem = value;
RaisePropertyChange("MySelectedItem");
}
}
}
public IList<MyModel> MyItems
{
get
{
return new List<MyModel>() {new MyModel(){Name="A"},
new MyModel(){Name="B"},
new MyModel(){Name="C"}};
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChange(string name)
{
if(PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
One MyItems property and one SelectedItem property
and Click handler like
private void Button_Click_1(object sender, RoutedEventArgs e)
{
Random r = new Random();
int icnt = r.Next(0,3);
model.MySelectedItem = model.MyItems[icnt];
}
I found that TextBlock.Text is updating but Combobox selected item is not updating. I try to dig out the reason and found that if I execute code below
MyModel prevItem = model.MyItems.Where((m) => m.Name.Equals("A")).FirstOrDefault();
MyModel newItem = model.MyItems.Where((m) => m.Name.Equals("A")).FirstOrDefault();
bool result = prevItem.Equals(newItem);
The value is always false. But why, why I am getting the new reference to same object from collection.
How can resolve this issue.
Thanks
you are getting a new reference because each time the binding mechanism will ask for MyItems you will create a new list.
try creating it once and use observable collection
You need to modify your MyItems code. You are getting new list every time. Try this out.
private List<MyModel> _myItems;
public IList<MyModel> MyItems
{
get
{
if (_myItems == null)
{
myItems = new List<MyModel>();
myItems.Add(new MyModel() { Name = "A" });
myItems.Add(new MyModel() { Name = "B" });
myItems.Add(new MyModel() { Name = "C" });
}
return _myItems}
}
}