Accessing Controls in DataTemplate in ListBox in WPF - c#

I have listbox which have DataTemplate.
I can not access controls which placed in the datatemplate.
How can I access to this controls?
<ListBox Height="344" Name="listBoxMedicine" Width="881">
<ListBox.ItemTemplate>
<DataTemplate >
<TextBlock Name="myTextBlock">
</Datatemplate>
</ListBox.ItemTemplate>
</ListBox>
Thank you for your attention.

If you still want access your controls in codebehaind, you can do something like this:
1) Add a new helper method somewhere:
public static IEnumerable<Visual> ToVisualTree(this Visual visual)
{
yield return visual;
int numVisuals = VisualTreeHelper.GetChildrenCount(visual);
for (int i = 0; i < numVisuals; ++i)
{
var child = (Visual)VisualTreeHelper.GetChild(visual, i);
if (child == null) yield break;
foreach (var subItem in child.ToVisualTree())
{
yield return subItem;
}
}
}
2) Use it like this:
var allTextBlocks = listBoxMedicine.ToVisualTree().OfType<TextBlock>().ToList();
But I still strongly recomend to refactor your data model.

Based on the comments i would suggest you create a view-model which simply provides a property for the visbility, e.g.:
public class DataViewModel : INotifyPropertyChanged
{
private Data _data;
// Some data property.
public Data Data { get { return _data; } set { ... } }
private Visibility _visibility;
// The visibility property.
public Visibility Visibility { get { return _visibility; } set { ... } }
}
You can then bind that visibility and later set it in code to affect the view:
<DataTemplate >
<TextBlock Name="myTextBlock" Visibility="{Binding Visibility}">
</Datatemplate>

I'm using this approach to get FrameworkElement from ItemsControl, also will work with ListBox, ListView because they all inherit from ItemsControl.
private void CheckBounds(ItemsControl itemsControl)
{
foreach (var item in itemsControl.Items)
{
var child = ((FrameworkElement)itemsControl.ItemContainerGenerator.ContainerFromItem(item));
child.IsEnabled = child.IsControlVisible(itemsControl);
}
}

Related

How to bind and display ListBoxItem index in ListBox?

I have a .NET 5 project with following Nuget Packages:
HandyControls for UI
Gong-Wpf-DragDrop for Drag and Drop elements in a List
I have a XAML with a ListBoxand a ViewModel with aObservableCollection` of Model.
The ObservableCollection is binded as ItemSource of ListBox
What I want to achieve:
When i Drag and Drop an item in a different position (or Add/Delete), I want the indexes to be refreshed.
Example:
Before Drag/Drop
After Drag/Drop
Actually, i binded the drophandler of gong-wpf-dragdrop
and at the end of the drop, i manually refresh every single Index in my list.
there is a way to do it easily? because actually i have to refresh indexes manually.
Summarizing:
When i reorder/delete/add items i want Model.Index of every item updated with the correct index position in ListBox.
My Mandate is:
Show index (one based)
Give the possibility to reorder the elements
I tried looking for similar questions but didn't find much that could help me.
Thanks in advance :)
Model:
public class Model : BindablePropertyBase
{
private int index;
private string name;
public int Index
{
get { return index; }
set
{
index = value;
RaisePropertyChanged();
}
}
public string Name
{
get { return name; }
set
{
name = value;
RaisePropertyChanged();
}
}
}
And below a xaml with a simple binded list
MainWindow.xaml
<hc:Window x:Class="ListBoxIndexTest.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:hc="https://handyorg.github.io/handycontrol"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:dd="clr-namespace:GongSolutions.Wpf.DragDrop;assembly=GongSolutions.Wpf.DragDrop"
xmlns:local="clr-namespace:ListBoxIndexTest"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="600">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid>
<ListBox ItemsSource="{Binding TestList}" dd:DragDrop.DropHandler="{Binding}"
dd:DragDrop.IsDropTarget="True" dd:DragDrop.IsDragSource="True">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin="10"
Background="Aqua">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Index}" Margin="10,0"/>
<TextBlock Text="{Binding Name}"
Grid.Column="1"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</hc:Window>
MainWindowViewModel.cs
public class MainWindowViewModel : BindablePropertyBase, IDropTarget
{
private ObservableCollection<Model> test_list;
public ObservableCollection<Model> TestList
{
get
{
return test_list;
}
set
{
test_list = value;
RaisePropertyChanged();
}
}
// Constructor
public MainWindowViewModel()
{
TestList = new ObservableCollection<Model>()
{
new Model()
{
Index = 1,
Name = "FirstModel"
},
new Model()
{
Index = 2,
Name = "SecondModel"
},
new Model()
{
Index = 3,
Name = "ThirdModel"
}
};
}
public void DragOver(IDropInfo dropInfo)
{
Model sourceItem = dropInfo.Data as Model;
Model targetItem = dropInfo.TargetItem as Model;
if (sourceItem != null && targetItem != null)
{
dropInfo.DropTargetAdorner = DropTargetAdorners.Insert;
dropInfo.Effects = DragDropEffects.Move;
}
}
public void Drop(IDropInfo dropInfo)
{
Model sourceItem = dropInfo.Data as Model;
Model targetItem = dropInfo.TargetItem as Model;
if(sourceItem != null && targetItem != null)
{
int s_index = sourceItem.Index - 1;
int t_index = targetItem.Index - 1;
TestList.RemoveAt(s_index);
TestList.Insert(t_index, sourceItem);
RefreshAllIndexes();
}
}
private void RefreshAllIndexes()
{
for (int i = 0; i < TestList.Count; i++)
{
TestList[i].Index = i + 1;
}
}
}
I don't believe there is an out-of-the box way to bind to a container index in WPF. Your solution is actually easy to understand.
If you find yourself binding often to index, you could create your own attached property/value converter that internally climbs up the visual tree using these helpers until it finds the parent ItemsControland makes use of the IndexFromContainer method.
Here is some code to get you started with this method:
First a small helper function to climb up the visual tree looking for an item of generic type:
public static DependencyObject FindParentOfType<T>(DependencyObject child) where T : DependencyObject {
//We get the immediate parent item
DependencyObject parentObject = VisualTreeHelper.GetParent(child);
//we've reached the end of the tree
if (parentObject == null) {
return null;
}
//check if the parent matches the type we're looking for
if (parentObject is T parent) {
return parent;
} else {
return FindParentOfType<T>(parentObject);
}
}
Then a value converter that takes a control as input value and returns its index in the first encountered ItemsControl:
public class ContainerToIndexConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
//Cast the passed value as an ItemsControl container
DependencyObject container = value as ContentPresenter;
if (container == null) {
container = value as ContentControl;
}
//Finds the parent ItemsControl by looking up the visual tree
var itemControls = (ItemsControl)FindParentOfType<ItemsControl>(container);
//Gets the index of the container from the parent ItemsControl
return itemControls.ItemContainerGenerator.IndexFromContainer(container);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
And this is how you would use it in XAML:
<ListBox.ItemTemplate>
<DataTemplate>
<!-- This will display the index of the list item. -->
<TextBlock Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContentPresenter}, Converter={StaticResource ContainerToIndexConverter}}" />
</DataTemplate>
</ListBox.ItemTemplate>

Access a Named Control Inside a XAML DataTemplate

I managed to access control in the datatemplate of a GridViewItem, the following code:
private void btnChangePhoneNumber_Click(object sender, RoutedEventArgs e)
{
GridCell.SelectedItem = GridCell.Items[3];
var container = GridCell.ContainerFromIndex(3);
var _children = AllChildren(container);
var _control = _children.First(c => c.Name == "PhoneNumber");
_control.text = "123456789";
}
public List<TextBlock> AllChildrenText(DependencyObject parent)
{
var _List = new List<TextBlock> { };
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
var _Child = VisualTreeHelper.GetChild(parent, i);
if (_Child is TextBlock)
{
_List.Add(_Child as TextBlock);
}
_List.AddRange(AllChildrenText(_Child));
}
return _List;
}
where the GridCell is a Gridview.
This work.. but..
If I implement GridView with less than 40 items it's all right.
Unlike if I implement gridView with 10000 items, the text change that happens with the method: btnChangePhoneNumber_Click, also happens in other items ... and I can not understand the reason since, in the btnChangePhoneNumber_Click method, only one item is chosen.
Thanks in advance. A greeting.
I have tested your code, but I could not reproduce your issue in my side. As far as I'm concerned, It is low performance to render 10000 items in your GridView. And using VisualTreeHelper will bring about worse performance. You could bind
the text of TextBlock in the datatemplate with mvvm ViewModel. You just need
to modify the view model and the text of TextBlock will be changed. For more please refer to Data binding in depth. And the following is segment code of ViewModel.
MainPageViewModel.cs
public class MainPageViewModel : ViewModelBase
{
private ObservableCollection<Phone> _items;
public ObservableCollection<Phone> Items
{
get
{
return _items;
}
set
{
_items = value;
OnPropertyChanged();
}
}
public MainPageViewModel()
{
var list = new ObservableCollection<Phone>();
for (var i = 0; i < 1000; i++)
{
list.Add(new Phone { PhoneNumber = "123456" });
}
_items = list;
}
}
MainPage.xaml
<Page.DataContext>
<local:MainPageViewModel x:Name="ViewModel"/>
</Page.DataContext>
<StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Button Click="btnChangePhoneNumber_Click" Content=" click me"/>
<GridView x:Name="GridCell" Height="400" ItemsSource="{Binding Items}" >
<GridView.ItemTemplate>
<DataTemplate x:DataType="local:Phone">
<TextBlock Text="{x:Bind PhoneNumber ,Mode=OneWay}"/>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
</StackPanel>
I have upload the code sample to github. Please check!

Binding in WPF not working as expected

I have a MainWindow.xaml, MainwindowViewModel.cs, HaemogramReport.xaml and HaemogramReport.xaml.cs. I have other files as well in my project, but the problem lies in the above mentioned four files.
I am posting the minimal code here so that others can catch the problem.
Now in HaemogramReport.xaml I declare some controls like Grid, TextBox, TextBlock, Rectangle, Border, ContentControl etc.
For example HaemogramReport.xaml looks like:
<Page.DataContext>
<vm:MainWindowViewModel />
</Page.DataContext>
<Grid DataContext="{Binding Source={StaticResource Settings}}" PreviewMouseDown="Object_Selection" x:Name="Root">
<Border Style="{StaticResource BorderStyle}" x:Name="HaemogramTestBorder"
Grid.Row="{Binding Default.HaemogramTestGridRow}" Grid.Column="{Binding Default.HaemogramTestGridColumn}"
Grid.RowSpan="{Binding Default.HaemogramTestGridRowSpan}" Grid.ColumnSpan="{Binding Default.HaemogramTestGridColumnSpan}">
<Grid>
<Rectangle Fill="Transparent" x:Name="HaemogramTestRectangle"/>
<TextBlock x:Name="HaemogramTestTextBlock"
Text="{Binding Default.HaemogramTestText}" Visibility="{Binding Default.HaemogramTestVisibility}"
Background="{Binding Default.HaemogramTestBackground, Converter={StaticResource colorToSolidColorBrushConverter}}"
Foreground="{Binding Default.HaemogramTestForeground, Converter={StaticResource colorToSolidColorBrushConverter}}"
FontFamily="{Binding Default.HaemogramTestFontFamily, Converter={StaticResource stringToFontFamilyConverter}}"
FontSize="{Binding Default.HaemogramTestFontSize}"
FontWeight="{Binding Default.HaemogramTestFontWeight}" FontStyle="{Binding Default.HaemogramTestFontStyle}"
HorizontalAlignment="{Binding Default.HaemogramTestHorizontalAlignment}"
VerticalAlignment="{Binding Default.HaemogramTestVerticalAlignment}"
Margin="{Binding Default.HaemogramTestMargin}" />
</Grid>
</Border>
</Grid>
When I click on any of the element in the above declared elements, the mousedown event of the grid named Root is raised.
That event handler is in HaemogramReport.xmal.cs. Here it is:
private void Object_Selection(object sender, MouseButtonEventArgs e)
{
var mouseWasDownOn = e.Source as FrameworkElement;
if (mouseWasDownOn != null)
{
foreach (Border border in FindVisualChildren<Border>(Root))
{
border.BorderBrush = Brushes.Transparent;
}
if (!(mouseWasDownOn is Border))
{
FindParent<Border>(mouseWasDownOn).BorderBrush = Brushes.Orange;
}
MainWindowViewModel mwvm = new MainWindowViewModel();
mwvm.SelectedObj = mouseWasDownOn;
}
}
public static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj != null)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
if (child != null && child is T)
{
yield return (T)child;
}
foreach (T childOfChild in FindVisualChildren<T>(child))
{
yield return childOfChild;
}
}
}
}
public static T FindParent<T>(DependencyObject child) where T : DependencyObject
{
//get parent item
DependencyObject parentObject = VisualTreeHelper.GetParent(child);
//we've reached the end of the tree
if (parentObject == null) return null;
//check if the parent matches the type we're looking for
T parent = parentObject as T;
if (parent != null)
return parent;
else
return FindParent<T>(parentObject);
}
In mouseDown handler of Grid named Root, I say mwvm.SelectedObj = mouseWasDownOn;
SelectedObj is a property of type FrameworkElement which is declared in MainwindowViewModel.cs as follows:
private FrameworkElement selectedObj;
public FrameworkElement SelectedObj
{
get
{
return selectedObj;
}
set
{
selectedObj = value;
OnPropertyChanged("SelectedObj");
}
}
Now in my MainWindow I have for example a grid and a textBox inside it. The problematic bindings are declared here. xaml looks like:
<Window.DataContext>
<vm:MainWindowViewModel />
</Window.DataContext>
<Grid DataContext="{Binding SelectedObj, UpdateSourceTrigger=PropertyChanged}">
<TextBox Text="{Binding Text, UpdateSourceTrigger=PropertyChanged, TargetNullValue='null', FallbackValue='Error'}"/>
</Grid>
When using the above code, I always get the Text Error in above TextBox.
At the first chance I thought that this might be the binding error, so I changed my MainWindowViewModel.cs as follows:
public class MainWindowViewModel : INotifyPropertyChanged
{
public MainWindowViewModel()
{
SelectedObj = txt;
}
TextBlock txt = new TextBlock()
{
Text = "123"
};
private FrameworkElement selectedObj;
public FrameworkElement SelectedObj
{
get
{
return selectedObj;
}
set
{
selectedObj = value;
OnPropertyChanged("SelectedObj");
}
}
}
After making the above changes when I run my project I can see 123 in textbox but when I click on any element the text in the textbox does not change.
Now the question here is that if its a binding error then why in second example I get 123 in textbox while in 1st example I get Error - the fallback value.
And if it's not a binding error then what is the problem in above code?
Update
When I debug, I found that get part of SelectedObj is never called. But I don't know why?
Update -- Reed Copsey
Here is my new class:
public class DesignMethods
{
public static void FindCurrentlyClickedElement(DependencyObject Root, MouseButtonEventArgs e, MainWindowViewModel vm)
{
var mouseWasDownOn = e.OriginalSource as FrameworkElement;
if (mouseWasDownOn != null)
{
foreach (Border border in FindVisualChildren<Border>(Root))
{
border.BorderBrush = Brushes.Transparent;
}
if (!(mouseWasDownOn is Border))
{
FindParent<Border>(mouseWasDownOn).BorderBrush = Brushes.Orange;
}
vm.SelectedObj = mouseWasDownOn;
}
}
public static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj != null)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
if (child != null && child is T)
{
yield return (T)child;
}
foreach (T childOfChild in FindVisualChildren<T>(child))
{
yield return childOfChild;
}
}
}
}
public static T FindParent<T>(DependencyObject child) where T : DependencyObject
{
//get parent item
DependencyObject parentObject = VisualTreeHelper.GetParent(child);
//we've reached the end of the tree
if (parentObject == null) return null;
//check if the parent matches the type we're looking for
T parent = parentObject as T;
if (parent != null)
return parent;
else
return FindParent<T>(parentObject);
}
}
And I use it like:
private void Object_Selection(object sender, MouseButtonEventArgs e)
{
DesignMethods.FindCurrentlyClickedElement(Root, e, this.DataContext as MainWindowViewModel);
}
The problem is you're creating a new instance of the ViewModel, not using the existing one:
// This is not the same instance you're binding to!
// MainWindowViewModel mwvm = new MainWindowViewModel();
// Get the existing one instead
var mwvm = this.DataContext as MainWindowViewModel;
mwvm.SelectedObj = mouseWasDownOn;
Note that I would likely not use the term "ViewModel" here, though. What you are doing is very much not a typical MVVM scenario as you're tightly coupling your DataContext instance into your View, with coupling happening in both directions, which is pretty much the opposite of the normal goals of MVVM.
Edit:
You may also need to update your bindings for SelectedObj. I would recommend trying with the XAML set to:
<Grid>
<TextBox Text="{Binding SelectedObj.Text, UpdateSourceTrigger=PropertyChanged, TargetNullValue='null', FallbackValue='Error'}"/>
</Grid>
Try to use the OriginalSource instead of Source:
var mouseWasDownOn = e.OriginalSource as FrameworkElement;
because the Source property when dealing with composite controls, can be the parent that contains the OriginalSource object (in your case the grid).
I think your error might be that FrameworkElement doesn't have a Text property http://msdn.microsoft.com/en-us/library/system.windows.frameworkelement(v=vs.110).aspx
EDIT: Try updating your binding on Text to be
{Binding SelectedObj.Text}
I think your mistake might be that you are using "common" properties instead of DependencyProperties.
As you can see on Microsoft Description
"When you define your own properties and want them to support many
aspects of Windows Presentation Foundation (WPF) functionality,
including styles, data binding, inheritance, animation, and default
values, you should implement them as a dependency property."
These are the correct types of property to fully use all resources provided by WPF
Take a look at these links
http://msdn.microsoft.com/en-us/library/system.windows.dependencyproperty(v=vs.110).aspx
http://msdn.microsoft.com/en-us/library/ms750428(v=vs.110).aspx
Or simply look for Dependency Property WCF on google.
Another useful link to understand the difference between these properties is
https://stackoverflow.com/a/3674727
which I used when I had similar problems.
Hope it helps!

WPF Listview item highlighting

I have a listview which contains the customer informations. There is a search text box above the that listview. When you type anything into the textbox then it higlights the matched item in the listview. But , the problem is that it makes search only in the visual side of the listview. It doesn't search in the not scrolled side of the listview(buttom of the listview). My code is below. Please have a look.
private void FindListViewItem(DependencyObject obj)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
ListViewItem lv = obj as ListViewItem;
if (lv != null)
{
HighlightText(lv);
}
FindListViewItem(VisualTreeHelper.GetChild(obj as DependencyObject, i));
}
}
private void HighlightText(Object itx)
{
if (itx != null)
{
if (itx is TextBlock)
{
Regex regex = new Regex("(" +TxtSearch.Text + ")", RegexOptions.IgnoreCase);
TextBlock tb = itx as TextBlock;
if (TxtSearch.Text.Length == 0)
{
string str = tb.Text;
tb.Inlines.Clear();
tb.Inlines.Add(str);
return;
}
string[] substrings = regex.Split(tb.Text);
tb.Inlines.Clear();
foreach (var item in substrings)
{
if (regex.Match(item).Success)
{
Run runx = new Run(item);
runx.Background = Brushes.Lime;
tb.Inlines.Add(runx);
if (tb.IsMouseOver)
{
tb.IsEnabled = false;
}
}
else
{
tb.Inlines.Add(item);
tb.IsEnabled = false;
}
}
return;
}
else
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(itx as DependencyObject); i++)
{
HighlightText(VisualTreeHelper.GetChild(itx as DependencyObject, i));
}
}
}
}
This happens because the ListView, by default, uses virtualization for its content. This means that the ListViewItems are created when they are needed. If you didn't scroll the ListView, some ListViewItems will not be created and VisualTreeHelper.GetChildrenCount will not be able to return those ListViewItems.
To achieve what you want, you can:
disable ListView virtualization by setting: VirtualizingStackPanel.IsVirtualizing="False" on your ListView (not recommended if you have many items in your list).
you can enforce the creation of the ListViewItem which are not visible by calling IItemContainerGenerator.GenerateNext and IItemContainerGenerator.PrepareItemContainer (not recommended at all). (also take a look at this)
find a better logic to highlight your ListViewItems :) (recommended). (for example search on your collection for the items you want to highlight instead of searching on the UI elements that are only displaying your items. Then mark the items found as highlighted and base on this, display the ListViewItems accordingly (with a different template or style))
You can do this in several ways. Here is one way that i think would work in your scenario and partially with your code and still use virtualization.
Use a data template for the list view item, and create an event handler for loaded event, something like:
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" Loaded="FrameworkElement_OnLoaded"/>
</DataTemplate>
</ListView.ItemTemplate>
In the OnLoaded event handler call your HighlightText method on the sender:
HighlightText(sender)
In order to trigger the loaded event you'll need to refresh the list view each time the search string will change. Something like ListView.Items.Refresh() should do it.
You could improve this a bit by adding a small timer on the search text changed, so the user will be able to finish typing when it's searching for something.
There are other, more elegant ways to handle this, but for your case i think this should work.
In Addition to my Comment:
Use a Property and a Observable Collection and directly filter on that Collection.
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public ObservableCollection<Entry> MyCollection {get;set;}
public MainWindow()
{
InitializeComponent();
MyCollection = new ObservableCollection<Entry>();
MyCollection.Add(new Entry() { Name = "Test" });
MyCollection.Add(new Entry() { Name = "ABCD" });
MyCollection.Add(new Entry() { Name = "TESTABC" });
MyCollection.Add(new Entry() { Name = "BCDtest" });
this.MyListView.DataContext = this;
}
private void searchTerm_KeyUp(object sender, KeyEventArgs e)
{
String term = ((TextBox)sender).Text;
foreach (Entry entry in this.MyCollection)
{
if (entry.Name.Contains(term))
entry.Highlight();
else
entry.UnHighlight();
}
}
}
public class Entry : INotifyPropertyChanged
{
public String Name { get; set; }
public Color BGColor { get; set; }
public SolidColorBrush BGBrush
{
get
{
return new SolidColorBrush(this.BGColor);
}
}
public Entry()
{
this.UnHighlight();
}
public void Highlight()
{
this.BGColor = Colors.Yellow;
this.NotifyPropertyChanged("BGBrush");
}
public void UnHighlight()
{
this.BGColor = Colors.White;
this.NotifyPropertyChanged("BGBrush");
}
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
along with
<Grid>
<DockPanel>
<TextBox DockPanel.Dock="Top" Name="searchTerm" KeyUp="searchTerm_KeyUp"></TextBox>
<ListView Name="MyListView" ItemsSource="{Binding MyCollection}" >
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Background="{Binding BGBrush}" Text="{Binding Name}"></TextBlock>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</DockPanel>
</Grid>
And you are done. No need to manually touch the listview at any time. (To Increase Performance: For the Raising of the PropertyChanged Event you may want to add a check, if its really changing, or if it has been set to white from white etc.)

ListBox in which each item contains a List

I am trying to implement a list that contains items of a certain type, a Session. Each Session contains a list that contains the type Note. I want to display these Notes in the list under their respective Session header.
Currently I have tried two different methods. The first way was to use ItemsControls as ControlTemplate for the ListBoxItems. This is what I used in the picture below and it is how I want the list to look like. Each red rectangle shows a Session, the items below the header are the Notes. The problem then is that the selection from the ListBox selects ItemsControls instead of each separate Note.
The other way I tried to implement the list is to give each Note a property of which Session it belongs to in order to use a GroupStyle on the ListBox. If I then set the ItemsSource of the ListBox to a list of Notes instead of Sessions I'll get a list that looks like the picture and that has selection of notes. The problem now is that I want the list to show Sessions that doesn't contain any Notes as well.
Does anyone know what I should use to implement a list with selection and that works the way I have described?
MainWindow.xaml:
<TreeView ItemsSource="{Binding}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:Session}" ItemsSource="{Binding Path=Notes}">
<TextBlock Text="{Binding Path=Name}" />
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type local:Note}">
<Expander Header="{Binding Path=Notek}">
<TextBlock Foreground="Red" Text="{Binding Path=Details}" />
</Expander>
</DataTemplate>
</TreeView.Resources>
</TreeView>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
List<Session> sessions = new List<Session>();
for (int i = 0; i < 5; i++)
{
List<Note> notes = new List<Note>();
for (int j = i * 5; j < (i + 1) * 5; j++)
{
Note note = new Note()
{
Notek = string.Format("Note {0}", j),
Details = string.Format("Note j = {0}{1}j*j = {2}", j, System.Environment.NewLine, j*j)
};
notes.Add(note);
}
Session session = new Session()
{
Name = string.Format("Session # {0}", i),
Notes = notes
};
sessions.Add(session);
}
DataContext = sessions;
}
}
public class Session
{
public string Name { get; set; }
public List<Note> Notes { get; set; }
}
public class Note
{
public string Notek { get; set; }
public string Details { get; set; }
}
I think that you can style your HierarchicalDataTemplate as you want. I just show you the example. I think its easier rather than ItemsControl with event handlers.
To create the answer I will assume the following data model:
class Session
{
public IEnumerable<Note> Notes { get; }
}
class Note { }
This requires some coding to sync up the list boxes. I have created an attached property called 'ListBoxGroup'. All listboxes with the same group name can only have a single shared selected item. It is quite a lot of code so it's at the bottom.
Important to note: The listboxgroup for a listbox cannot be changed after originally set, and it doesn't support removal of items, doesn't check for nulls etc. So if you need to change sessions at runtime you should remove items from their groups, check if a listbox is removed from the visual tree, etc.
First the XAML for the page:
xmlns:local="clr-namespace:YourApplication.YourNamespace"
<!-- ItemsControl does not have selection -->
<ItemsControl ItemsSource="{Binding SessionList}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<!-- Header for the session -->
<Border Background="Gray">
<TextBlock Text="{Binding Name}" />
</Border>
<!-- listbox for notes -->
<ListBox ItemsSource="{Binding Notes}" local:ListBoxGroup.GroupName="Group1">
<ListBox.ItemTemplate>
<DataTemplate>
<!-- Template for a single note -->
<TextBlock Text="{Binding Description}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Below is C# code for the ListBoxGroup property:
public static class ListBoxGroup
{
public static string GetGroupName(DependencyObject obj)
{
return (string)obj.GetValue(GroupNameProperty);
}
public static void SetGroupName(DependencyObject obj, string value)
{
obj.SetValue(GroupNameProperty, value);
}
// Using a DependencyProperty as the backing store for GroupName. This enables animation, styling, binding, etc...
public static readonly DependencyProperty GroupNameProperty =
DependencyProperty.RegisterAttached("GroupName", typeof(string), typeof(ListBoxGroup), new UIPropertyMetadata(null, ListBoxGroupChanged));
private static Dictionary<string, List<ListBox>> _listBoxes = new Dictionary<string, List<ListBox>>();
private static void ListBoxGroupChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
string newValue = e.NewValue as string;
ListBox listBox = obj as ListBox;
if (newValue == null || listBox == null) return;
if (_listBoxes.ContainsKey(newValue))
{
_listBoxes[newValue].Add(listBox);
}
else
{
_listBoxes.Add(newValue, new List<ListBox>() { listBox });
}
listBox.SelectionChanged += new SelectionChangedEventHandler(listBox_SelectionChanged);
listBox.PreviewKeyUp += new System.Windows.Input.KeyEventHandler(listBox_KeyUp);
}
static void listBox_KeyUp(object sender, System.Windows.Input.KeyEventArgs e)
{
ListBox listBox = sender as ListBox;
if (e.Key == System.Windows.Input.Key.Up && listBox.SelectedIndex == 0)
{
//move to previous
string groupName = GetGroupName(listBox);
List<ListBox> group = _listBoxes[groupName];
int senderIndex = group.IndexOf(listBox);
if (senderIndex != 0)
{
listBox.SelectedItem = null;
ListBox beforeSender = group[senderIndex - 1];
int index = beforeSender.Items.Count - 1;
beforeSender.SelectedIndex = index;
var container = beforeSender.ItemContainerGenerator.ContainerFromIndex(index);
(container as FrameworkElement).Focus();
}
}
else if (e.Key == System.Windows.Input.Key.Down
&& listBox.SelectedIndex == listBox.Items.Count - 1)
{
//move to next
string groupName = GetGroupName(listBox);
List<ListBox> group = _listBoxes[groupName];
int senderIndex = group.IndexOf(listBox);
if (senderIndex != group.Count - 1)
{
listBox.SelectedItem = null;
ListBox afterSender = group[senderIndex + 1];
afterSender.SelectedIndex = 0;
var container = afterSender.ItemContainerGenerator.ContainerFromIndex(0);
(container as FrameworkElement).Focus();
}
}
}
static void listBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count > 0)
{
ListBox listBox = sender as ListBox;
string groupName = GetGroupName(listBox);
foreach (var item in _listBoxes[groupName])
{
if (item != listBox)
{
item.SelectedItem = null;
}
}
}
}
}

Categories