My scenario as below:
<ListView ItemsSource={Binding TestList}>
<ListView.ItemTemplate>
<DataTemplate>
<Expander Header="{Binding Key, BindingMode=OneWay}">
<Expander.Content>
<ListView ItemsSource={Binding Value}>
<ListView.ItemContainerStyle>
<Style TargetType = {ListViewItem}>
<Setter Property = "IsSelected" Value={Binding IsSelected}/>
</Style>
</ListView.ItemContainerStyle>
</ListView>
</Expander.Content>
</Expander>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
TestList:
Dictionary<string,ObservableCollection<Test>> TestList;
Test:
class Test : NotificationObject
{
public string Name { get; set;}
private bool isSelected;
public bool IsSelected
{
if( value != isSelected )
{
isSelected = value;
RaisePropertyChanged("IsSelected");
}
}
}
The inner ListView SelectionMode is Single, I can use keyboard up and down buttons to navigate items in the ListView. Is there a way to implement this scenario: When the focus is on the last item of a ListView, press down key, the focus is on the first item of the next ListView; when the focus is on the first item of a ListView, press up key, the focus is on the last item of the previous ListView.
Anyone can help?
Solution
public static List<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
{
List<T> list = new List<T>();
if (depObj != null)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
if (child != null && child is T)
{
list.Add((T)child);
}
List<T> childItems = FindVisualChildren<T>(child);
if (childItems != null && childItems.Count() > 0)
{
foreach (var item in childItems)
{
list.Add(item);
}
}
}
}
return list;
}
private void ListView_PreviewKeyDown(object sender, KeyEventArgs e)
{
ListView currentListView = sender as ListView;
List<ListView> listviews = FindVisualChildren<ListView>(ModulesListView);
int currentListViewIndex = listviews.IndexOf(currentListView);
// If the press key is Down and the selected item is the last one
if (Keyboard.IsKeyDown(Key.Down)&& currentListView.SelectedIndex + 1 == currentListView.Items.Count)
{
if (currentListViewIndex + 1 < listviews.Count)
{
// Get next ListView
var nextListView = listviews.ElementAt(currentListViewIndex + 1);
if (nextListView.Items != null && nextListView.Items.Count > 0)
{
ListViewItem item = nextListView.ItemContainerGenerator.ContainerFromIndex(0) as ListViewItem;
item.Focus();
item.IsSelected = true;
e.Handled = true;
}
}
}
else if (Keyboard.IsKeyDown(Key.Up)&& currentListView.SelectedIndex == 0)
{
if (currentListViewIndex > 0)
{
var previousListView = listviews.ElementAt(currentListViewIndex - 1);
if (previousListView.Items != null && previousListView.Items.Count > 0)
{
ListViewItem item = previousListView.ItemContainerGenerator.ContainerFromIndex(previousListView.Items.Count - 1) as ListViewItem;
item.Focus();
item.IsSelected = true;
e.Handled = true;
}
}
}
}
This problem has been solved! I have paste the solution below the question, I hope it could help other members who has the same demand, thanks!
Here you go, I tried to solve it by attached properties
using attached you can implement attachable behavior which are difficult to implement in xaml
here is what you need
xaml
<Grid xmlns:l="clr-namespace:CSharpWPF">
<ListView l:NavigationHelper.IsEnabled="True">
<sys:String>item 1</sys:String>
<sys:String>item 2</sys:String>
<sys:String>item 3</sys:String>
<sys:String>item 4</sys:String>
<ListView.ItemTemplate>
<DataTemplate>
<Expander Header="{Binding}">
<Expander.Content>
<ListView ItemsSource="{Binding}"
l:NavigationHelper.IsEnabled="True">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="IsSelected"
Value="{Binding IsSelected}" />
</Style>
</ListView.ItemContainerStyle>
</ListView>
</Expander.Content>
</Expander>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
I have attached a property l:NavigationHelper.IsEnabled="True" to my list views in order to enable the behavior
NavigationHelper.cs
namespace CSharpWPF
{
public class NavigationHelper : DependencyObject
{
public static bool GetIsEnabled(DependencyObject obj)
{
return (bool)obj.GetValue(IsEnabledProperty);
}
public static void SetIsEnabled(DependencyObject obj, bool value)
{
obj.SetValue(IsEnabledProperty, value);
}
// Using a DependencyProperty as the backing store for IsEnabled. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(NavigationHelper), new PropertyMetadata(false, OnIsEnabledChanged));
private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Selector selector = d as Selector;
if ((bool)e.NewValue)
selector.PreviewKeyDown += selector_PreviewKeyDown;
else
selector.PreviewKeyDown -= selector_PreviewKeyDown;
}
static void selector_PreviewKeyDown(object sender, KeyEventArgs e)
{
Selector selector = sender as Selector;
if (selector.Items.Count == 0)
return;
ListViewItem itemToSelect = null;
if (e.Key == Key.Up && selector.SelectedIndex == 0)
{
itemToSelect = selector.ItemContainerGenerator.ContainerFromIndex(selector.Items.Count - 1) as ListViewItem;
}
else if (e.Key == Key.Down && selector.SelectedIndex == selector.Items.Count - 1)
{
itemToSelect = selector.ItemContainerGenerator.ContainerFromIndex(0) as ListViewItem;
}
if (itemToSelect != null)
{
selector.SelectedItem = itemToSelect;
itemToSelect.Focus();
e.Handled = true;
}
}
}
}
in this class I have created an attached property and attached PreviewKeyDown when enabled
and in the event handler I am checking if the up key is pressed and the selection is on top then choose the last item and select it and move focus
same with the bottom most item and key is down and choose the first item in this case.
and most important mark the event consumed by setting e.Handled = true so listview does not process it further.
Related
I have a strange effect in my datagrid. In this sample the datagrid has 200 rows and the first column has a incrementing index. The datagrid has a MaxHeight property, so i see after loading only the first 30 rows.
I can scroll down the 200 rows but i never see in the first column the numbers 30-200 only repeatly 0-29!?! (I checked that the collection has the right values)
If I change the columns from DataGridTemplateColumn to DataGridTextColumn, I see all the values but this is not what i want.
Has someone an idea, why the cell content not show the right value?
Here is my code. This is a reduced sample of a large MVVM project. Please be lenient of this construction.
<Window.Resources>
<local:RowCellConverter x:Key="rcconv" />
<DataTemplate DataType="{x:Type local:BusinessDataGrid}">
<Grid
Margin="5"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Transparent"
Focusable="False"
Visibility="Visible">
<DataGrid
Name="dataGrid"
Height="700"
MaxHeight="600"
AutoGenerateColumns="False"
CanUserAddRows="False"
CanUserDeleteRows="False"
CanUserReorderColumns="False"
CanUserResizeColumns="True"
CanUserResizeRows="False"
CanUserSortColumns="False"
ColumnWidth="*"
EnableColumnVirtualization="True"
EnableRowVirtualization="True"
Initialized="dataGrid_Initialized"
ItemsSource="{Binding rows}"
ScrollViewer.CanContentScroll="True"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"
SelectionMode="Single"
SelectionUnit="CellOrRowHeader">
<DataGrid.Resources>
<DataTemplate x:Key="MyFieldCell" DataType="DataGridTemplateColumn">
<StackPanel>
<TextBlock Background="LightSalmon">Hallo</TextBlock>
<TextBox
x:Name="TableCell"
DataContext="{Binding RelativeSource={RelativeSource AncestorType=DataGridCell}, Converter={StaticResource rcconv}}"
IsReadOnly="False"
Background="{Binding Path=StateColor}"
Text="{Binding Path=MyValue}" />
</StackPanel>
</DataTemplate>
</DataGrid.Resources>
</DataGrid>
</Grid>
</DataTemplate>
</Window.Resources>
<Grid>
<ItemsControl Name="iControl" ItemsSource="{Binding Path=MainWindow.bFields}" />
</Grid>
And the code behind:
public partial class MainWindow : Window
{
public ObservableCollection<BusinessField> bFields = new ObservableCollection<BusinessField>();
public MainWindow()
{
BusinessDataGrid bdg = new BusinessDataGrid();
foreach (string col in new string[] { "Col1", "Col2", "Col3", "Col4", "Col5", "Col6", })
{
bdg.cols.Add(col);
}
for (int i = 0; i < 200; i++)
{
FieldRow fr = new FieldRow();
foreach(string col in bdg.cols)
{
FieldCell fc = new FieldCell();
fc.MyValue = string.Format("{0:000}{1}", i, col);
fr.cells.Add(fc);
}
bdg.rows.Add(fr);
}
InitializeComponent();
ItemsControl ic = iControl;
ic.ItemsSource = bFields;
bFields.Add(bdg);
}
private void dataGrid_Initialized(object sender, EventArgs e)
{
DataGrid dg = sender as DataGrid;
if (dg != null)
{
BusinessDataGrid bdg = dg.DataContext as BusinessDataGrid;
if (bdg != null)
bdg.OnUIInitialized(dg);
}
}
}
public class BusinessField : INotifyPropertyChanged
{
private PropertyChangedEventHandler propertyChangedEvent;
public void SendPropertyChanged(string propertyName)
{
VerifyCalledOnUIThread();
if (propertyChangedEvent != null)
propertyChangedEvent(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged
{
add
{
VerifyCalledOnUIThread();
propertyChangedEvent += value;
}
remove
{
VerifyCalledOnUIThread();
propertyChangedEvent -= value;
}
}
[Conditional("Debug")]
protected void VerifyCalledOnUIThread()
{
Debug.Assert(Dispatcher.CurrentDispatcher == Dispatcher.CurrentDispatcher, "Call must be made on UI thread.");
}
}
public class BusinessDataGrid:BusinessField
{
public List<string> cols = new List<string>();
public ObservableCollection<FieldRow> rows = new ObservableCollection<FieldRow>();
public void OnUIInitialized(DataGrid datagrid)
{
DataTemplate dt = (DataTemplate)datagrid.Resources["MyFieldCell"];
datagrid.Columns.Clear();
foreach(string col in cols)
{
DataGridTemplateColumn dgtc = new DataGridTemplateColumn()
{
CellTemplate = dt,
Visibility = Visibility.Visible,
Header = col,
SortMemberPath=col,
};
datagrid.Columns.Add(dgtc);
}
datagrid.ItemsSource = rows;
}
}
public class RowCellConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
DataGridCell cell = value as DataGridCell;
if((string)parameter == "FieldCell")
{
}
if (cell == null)
return null;
DataGridCellsPresenter dgcp = TreeHelper.GetVisualParent<DataGridCellsPresenter>(cell);
int ci = dgcp.ItemContainerGenerator.IndexFromContainer(cell);
FieldRow fr = cell.DataContext as FieldRow;
if (fr == null)
return null;
object ret = null;
switch((string)parameter)
{
case "StateColor":
ret = fr.cells[ci].StateColor;
break;
case "MyValue":
ret = fr.cells[ci].MyValue;
break;
default:
ret = fr.cells[ci];
break;
}
return ret;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class TreeHelper
{
#endregion
public static T GetVisualChild<T>(DependencyObject obj) where T : DependencyObject
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
if (child != null && child is T)
return (T)child;
else
{
T childOfChild = GetVisualChild<T>(child);
if (childOfChild != null)
return childOfChild;
}
}
return null;
}
public static T GetVisualParent<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 GetVisualParent<T>(parentObject);
}
}
public enum FieldCellState
{
Ok,
Error
}
public class FieldRow
{
public ObservableCollection<FieldCell> cells = new ObservableCollection<FieldCell>();
}
public class FieldCell : INotifyPropertyChanged
{
public string Colname;
private string myValue;
public override string ToString()
{
return MyValue;
}
public string MyValue
{
get { return myValue; }
set { myValue = value; }
}
public FieldCellState MyState
{
get { return (MyValue.Contains("7")) ? FieldCellState.Error : FieldCellState.Ok; }
}
public Brush StateColor
{
get { return (MyState == FieldCellState.Ok) ? new SolidColorBrush(Colors.LightGreen) : new SolidColorBrush(Colors.LightSalmon); }
}
private PropertyChangedEventHandler propertyChangedEvent;
public void SendPropertyChanged(string propertyName)
{
VerifyCalledOnUIThread();
if (propertyChangedEvent != null)
propertyChangedEvent(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged
{
add
{
VerifyCalledOnUIThread();
propertyChangedEvent += value;
}
remove
{
VerifyCalledOnUIThread();
propertyChangedEvent -= value;
}
}
[Conditional("Debug")]
protected void VerifyCalledOnUIThread()
{
Debug.Assert(Dispatcher.CurrentDispatcher == Dispatcher.CurrentDispatcher, "Call must be made on UI thread.");
}
}
It is because you have these two options enabled
EnableColumnVirtualization="True"
EnableRowVirtualization="True"
It try to virtualize it for you. Try to make them False and see if you can see your content correctly. Keep in mind setting them to false it will make your DataGrid to load much slower.
Try to do this for each column instead of making a template at resource level. This is just an example you can adapt it to your style.
<DataGridTemplateColumn Header="Some Name" IsReadOnly="False" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<RichTextBox>
<FlowDocument IsOptimalParagraphEnabled="True" IsHyphenationEnabled="True">
<Paragraph FontFamily="Segoe UI" FontSize="14">
<Run Text="{Binding Path=First ,Mode=TwoWay , UpdateSourceTrigger=PropertyChanged}" />
<Run Text="{Binding Path=FirstText ,Mode=TwoWay , UpdateSourceTrigger=PropertyChanged}" />
<Run Text="{Binding Path=SearchedText ,Mode=TwoWay , UpdateSourceTrigger=PropertyChanged}" Background="#FFE34C"/>
<Run Text="{Binding Path=SecondText ,Mode=TwoWay , UpdateSourceTrigger=PropertyChanged}" />
</Paragraph>
</FlowDocument>
</RichTextBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
Seems you are using a third party grid ? issue is with how it is virtualized
i had this issue once and the reason was I was using an Identity column of the grid and hence the column was auto-generated.
the grid is being virtualized and hence cant calculate no of rows above or below the view.
you can either stop virtualization if you don't need it (200 rows or so wont make any difference)
or add a new column that is bound to index of item in the collection, that should work
I'm using a Datepicker control inside a DataGrid. After changing a day and pressing the ENTER key, the Selected row remains on the same Datagrid row and does not change to the next Datagrid row.
Below is my DatePicker control:
<DataGridTemplateColumn x:Name="startDateColumn" Width="*" Header="Start Date">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding StartDate, StringFormat=d}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<DatePicker SelectedDate="{Binding StartDate...
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
I want to allow the user to move to the next Datagrid row after selecting a day and pressing ENTER.
How can I do that?
Thanks in advance...
Add this attached property class
public class EnterKeyTraversal
{
public static bool GetIsEnabled(DependencyObject obj)
{
return (bool)obj.GetValue(IsEnabledProperty);
}
public static void SetIsEnabled(DependencyObject obj, bool value)
{
obj.SetValue(IsEnabledProperty, value);
}
static void ue_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
var ue = e.OriginalSource as FrameworkElement;
var parent = GetVisualParent<DataGrid>(ue);
if (parent == null) return;
if (e.Key == Key.Enter)
{
e.Handled = true;
ue.MoveFocus(new TraversalRequest(FocusNavigationDirection.Down));
parent.SelectedIndex += 1;
}
}
private static void ue_Unloaded(object sender, RoutedEventArgs e)
{
var ue = sender as FrameworkElement;
if (ue == null) return;
ue.Unloaded -= ue_Unloaded;
ue.PreviewKeyDown -= ue_PreviewKeyDown;
}
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached("IsEnabled", typeof(bool),
typeof(EnterKeyTraversal), new UIPropertyMetadata(false, IsEnabledChanged));
static void IsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var ue = d as FrameworkElement;
if (ue == null) return;
if ((bool)e.NewValue)
{
ue.Unloaded += ue_Unloaded;
ue.PreviewKeyDown += ue_PreviewKeyDown;
}
else
{
ue.PreviewKeyDown -= ue_PreviewKeyDown;
}
}
public static T GetVisualParent<T>(DependencyObject child) where T : Visual
{
Visual parentObject = VisualTreeHelper.GetParent(child) as Visual;
if (parentObject == null) return null;
return parentObject is T ? parentObject as T : GetVisualParent<T>(parentObject);
}
}
Add style for DatePickerTextBox
<Style TargetType="{x:Type DatePickerTextBox}">
<Setter Property="local:EnterKeyTraversal.IsEnabled" Value="True"/>
</Style>
I have a ListView in my Windows Phone 8.1 in which multiple items can be selected. A few items are not selectable in it. I have defined a DataTemplate Selector for both kinds of items. The class which i am using for binding has an IsSelectable property which determines which DataTemplate is to be used.
<ListView SelectionMode="Multiple">
<ListView.ItemTemplate>
<DataTemplate x:Key="KeyValueDataTemplate">
<utils:KeyValueTemplateSelector HorizontalAlignment="Stretch" Content="{Binding}">
<utils:KeyValueTemplateSelector.NotSelectable>
<DataTemplate>
<!-- some controls -->
</DataTemplate>
</utils:KeyValueTemplateSelector.NotSelectable>
<utils:KeyValueTemplateSelector.Selectable>
<DataTemplate>
<!-- some controls -->
</DataTemplate>
</utils:KeyValueTemplateSelector.Selectable>
</utils:KeyValueTemplateSelector>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Using the SelectionMode="Multiple" option displays a CheckBox beside every item automatically. I need to hide the CheckBox for items which are not selectable. I tried editing the ListViewItem Style and binding the checkbox Visibility IsSelectable(using a BooleanToVisibilityConverter). But it doesnt seem to work.
In any part of your DataTemplate set this property:
local:SelectorHelper.IsItemSelectionEnabled="{Binding IsSelectable}"
Example:
<ListView.ItemTemplate>
<DataTemplate>
<Grid local:SelectorHelper.IsItemSelectionEnabled="{Binding IsSelectable}">
<!-- -->
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
And here is my SelectorHelper:
public static class SelectorHelper
{
public static readonly DependencyProperty IsItemSelectionEnabledProperty =
DependencyProperty.RegisterAttached("IsItemSelectionEnabled", typeof(bool), typeof(SelectorHelper), new PropertyMetadata(true, IsItemSelectionEnabledChanged));
public static void SetIsItemSelectionEnabled(DependencyObject dependencyObject, bool value)
{
dependencyObject.SetValue(IsItemSelectionEnabledProperty, value);
}
public static bool GetIsItemSelectionEnabled(DependencyObject dependencyObject)
{
return (bool)dependencyObject.GetValue(IsItemSelectionEnabledProperty);
}
private static void IsItemSelectionEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var frameworkElement = (FrameworkElement)d;
var selectorItem = XamlTreeHelper.FindParent<SelectorItem>(frameworkElement);
if (selectorItem != null)
{
selectorItem.IsEnabled = GetIsItemSelectionEnabled(frameworkElement);
}
else
{
frameworkElement.Loaded -= OnSelectableItemLoaded;
frameworkElement.Loaded += OnSelectableItemLoaded;
}
}
private static void OnSelectableItemLoaded(object sender, RoutedEventArgs e)
{
var frameworkElement = (FrameworkElement)sender;
var selectorItem = FindParent<SelectorItem>(frameworkElement);
if (selectorItem != null)
{
selectorItem.IsEnabled = GetIsItemSelectionEnabled(frameworkElement);
}
}
private static T FindParent<T>(DependencyObject child) where T : DependencyObject
{
var currentObject = child;
do
{
currentObject = VisualTreeHelper.GetParent(currentObject);
var parent = currentObject as T;
if (parent != null)
{
return parent;
}
} while (currentObject != null);
return null;
}
}
I want to be able to maintain a list in the background that puts new items at the end of the list (to avoid Insert() pushing the items around on updates) but to be able to display it in the reverse order without "sorting".
I just want it to show up in the list view in the reverse order that it is in the list. Can I do this with a template or something similar?
You can change the ListView's ItemsPanel to be a DockPanel with LastChildFill set to false. Then in the ItemContainerStyle, set the DockPanel.Dock property to bottom. This will start filling at the bottom and work its way up to the top. I put the ListView in a grid with 2 rows, first's Height="Auto" and second's Height="*" and it acted just like a normal ListView, but with the items reversed.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ListBox Grid.Row="0">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}"
BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="DockPanel.Dock"
Value="Bottom" />
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<DockPanel LastChildFill="False" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</Grid>
Idea taken from:
https://stackoverflow.com/a/493059/2812277
Update
Here is an Attached Behavior which will reverse any ItemsControl. Use it like this
<ListBox behaviors:ReverseItemsControlBehavior.ReverseItemsControl="True"
...>
ReverseItemsControlBehavior
public class ReverseItemsControlBehavior
{
public static DependencyProperty ReverseItemsControlProperty =
DependencyProperty.RegisterAttached("ReverseItemsControl",
typeof(bool),
typeof(ReverseItemsControlBehavior),
new FrameworkPropertyMetadata(false, OnReverseItemsControlChanged));
public static bool GetReverseItemsControl(DependencyObject obj)
{
return (bool)obj.GetValue(ReverseItemsControlProperty);
}
public static void SetReverseItemsControl(DependencyObject obj, object value)
{
obj.SetValue(ReverseItemsControlProperty, value);
}
private static void OnReverseItemsControlChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if ((bool)e.NewValue == true)
{
ItemsControl itemsControl = sender as ItemsControl;
if (itemsControl.IsLoaded == true)
{
DoReverseItemsControl(itemsControl);
}
else
{
RoutedEventHandler loadedEventHandler = null;
loadedEventHandler = (object sender2, RoutedEventArgs e2) =>
{
itemsControl.Loaded -= loadedEventHandler;
DoReverseItemsControl(itemsControl);
};
itemsControl.Loaded += loadedEventHandler;
}
}
}
private static void DoReverseItemsControl(ItemsControl itemsControl)
{
Panel itemPanel = GetItemsPanel(itemsControl);
itemPanel.LayoutTransform = new ScaleTransform(1, -1);
Style itemContainerStyle;
if (itemsControl.ItemContainerStyle == null)
{
itemContainerStyle = new Style();
}
else
{
itemContainerStyle = CopyStyle(itemsControl.ItemContainerStyle);
}
Setter setter = new Setter();
setter.Property = ItemsControl.LayoutTransformProperty;
setter.Value = new ScaleTransform(1, -1);
itemContainerStyle.Setters.Add(setter);
itemsControl.ItemContainerStyle = itemContainerStyle;
}
private static Panel GetItemsPanel(ItemsControl itemsControl)
{
ItemsPresenter itemsPresenter = GetVisualChild<ItemsPresenter>(itemsControl);
if (itemsPresenter == null)
return null;
return GetVisualChild<Panel>(itemsControl);
}
private static Style CopyStyle(Style style)
{
Style styleCopy = new Style();
foreach (SetterBase currentSetter in style.Setters)
{
styleCopy.Setters.Add(currentSetter);
}
foreach (TriggerBase currentTrigger in style.Triggers)
{
styleCopy.Triggers.Add(currentTrigger);
}
return styleCopy;
}
private static T GetVisualChild<T>(DependencyObject parent) where T : Visual
{
T child = default(T);
int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < numVisuals; i++)
{
Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
child = v as T;
if (child == null)
{
child = GetVisualChild<T>(v);
}
if (child != null)
{
break;
}
}
return child;
}
}
Otherwise, you can follow what's outlined in the following link: WPF reverse ListView
<ListBox ...>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel VerticalAlignment="Top" Orientation="Vertical">
<VirtualizingStackPanel.LayoutTransform>
<ScaleTransform ScaleX="1" ScaleY="-1" />
</VirtualizingStackPanel.LayoutTransform>
</VirtualizingStackPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="LayoutTransform">
<Setter.Value>
<ScaleTransform ScaleX="1" ScaleY="-1" />
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
After googling and trying things for half of a day I came across #ryan-west answer, and slightly modified it to fit my needs. I was able to get exactly what I wanted a listbox in a scrollviewer that showed a list exactly as normally seen, but in reverse order.
<ScrollViewer>
<ListBox ScrollViewer.HorizontalScrollBarVisibility="Disabled"
VerticalAlignment="Top"
ItemsSource="{Binding MyList, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<DockPanel>
<Image DockPanel.Dock="Left"
Source="MyIcon.png"
Width="16" />
<Label DockPanel.Dock="Left"
Content="{Binding MyName, Mode=TwoWay}"/>
</DockPanel>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}"
BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="DockPanel.Dock"
Value="Bottom" />
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<DockPanel LastChildFill="False" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</ScrollViewer>
Assuming that the ItemsSource is an ObservableCollection, my solution was to implement a ReverseObservableCollection:
public class ReverseObservableCollection<T> : IReadOnlyList<T>, INotifyCollectionChanged, INotifyPropertyChanged
{
#region Private fields
private readonly ObservableCollection<T> _observableCollection;
#endregion Private fields
#region Constructor
public ReverseObservableCollection(ObservableCollection<T> observableCollection)
{
_observableCollection = observableCollection;
observableCollection.CollectionChanged += ObservableCollection_CollectionChanged;
}
#endregion
#region Event handlers
private void ObservableCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (new[] { NotifyCollectionChangedAction.Add, NotifyCollectionChangedAction.Remove, NotifyCollectionChangedAction.Reset }.Contains(e.Action))
{
OnPropertyChanged(nameof(Count));
}
OnPropertyChanged(Binding.IndexerName); // ObservableCollection does this to improve WPF performance.
var newItems = Reverse(e.NewItems);
var oldItems = Reverse(e.OldItems);
int newStartingIndex = e.NewItems != null ? _observableCollection.Count - e.NewStartingIndex - e.NewItems.Count : -1;
//int oldCount = _observableCollection.Count - (e.NewItems?.Count ?? 0) + (e.OldItems?.Count ?? 0);
//int oldStartingIndex = e.OldItems != null ? oldCount - e.OldStartingIndex - e.OldItems.Count : -1;
int oldStartingIndex = e.OldItems != null ? _observableCollection.Count - e.OldStartingIndex - (e.NewItems?.Count ?? 0) : -1;
var eventArgs = e.Action switch
{
NotifyCollectionChangedAction.Add => new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, newItems, newStartingIndex),
NotifyCollectionChangedAction.Remove => new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, oldItems, oldStartingIndex),
NotifyCollectionChangedAction.Replace => new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newItems, oldItems, oldStartingIndex),
NotifyCollectionChangedAction.Move => new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, oldItems, newStartingIndex, oldStartingIndex),
NotifyCollectionChangedAction.Reset => new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset),
_ => throw new ArgumentException("Unexpected Action", nameof(e)),
};
OnCollectionChanged(eventArgs);
}
#endregion
#region IReadOnlyList<T> implementation
public T this[int index] => _observableCollection[_observableCollection.Count - index - 1];
public int Count => _observableCollection.Count;
public IEnumerator<T> GetEnumerator()
{
for (int i = _observableCollection.Count - 1; i >= 0; --i)
{
yield return _observableCollection[i];
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
#endregion
#region INotifyCollectionChanged implementation
public event NotifyCollectionChangedEventHandler? CollectionChanged;
private void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
{
CollectionChanged?.Invoke(this, args);
}
#endregion
#region INotifyPropertyChanged implementation
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged(string? propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
#region Private methods
private IList? Reverse(IList? list)
{
if (list == null) return null;
object[] result = new object[list.Count];
for (int i = 0; i < list.Count; ++i)
{
result[i] = list[list.Count - i - 1];
}
return result;
}
#endregion
}
Then, you just add a new property to the ViewModel and bind to it:
public class ViewModel
{
// Your old ItemsSource:
public ObservableCollection<string> Collection { get; } = new ObservableCollection<string>();
// New ItemsSource:
private ReverseObservableCollection<string>? _reverseCollection = null;
public ReverseObservableCollection<string> ReverseCollection => _reverseCollection ??= new ReverseObservableCollection<string>(Collection);
}
I'm trying to select a TreeViewItem. Now, I have access to the containing TreeViewItem and have told it to expand so I can select its kid. If it's already expanded all is well, if it's not then I run this code:
EventHandler selector = new EventHandler(delegate
{
if (selectedDirectoryTreeItem.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
TreeViewItem want = selectedDirectoryTreeItem.ItemContainerGenerator.ContainerFromItem(dirWeWantSelected) as TreeViewItem;
if (want == null)
return;
want.IsSelected = true;
// selectedDirectoryTreeItem.ItemContainerGenerator.StatusChanged -= selector;
}
});
selectedDirectoryTreeItem.ItemContainerGenerator.StatusChanged += selector;
So my question is, why wont it select? want is always null. I'm scouring the interwebs looking for another way of doing this but it would be cool if somebody could explain this to me
I've personally always found it easiest to stick a Selected property into my model object and then just bind the TreeViewItem Selected property to the Selected property of the model. Here is some code:
Model
public class Data : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public Data()
{
DataItems = new List<Data>();
}
public string Name { get; set; }
private bool _selected;
public bool Selected
{
get { return _selected; }
set
{
_selected = value;
OnPropertyChanged("Selected");
}
}
public List<Data> DataItems { get; set; }
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
XAML
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
xmlns:controls="clr-namespace:MyControls;assembly=MyControls"
Title="Window1">
<Window.Resources>
<Style x:Key="CustomTreeViewItem" TargetType="TreeViewItem">
<Setter Property="IsSelected" Value="{Binding Path=Selected, Mode=TwoWay}" />
<Setter Property="IsExpanded" Value="True" />
</Style>
</Window.Resources>
<DockPanel Background="Transparent">
<TreeView x:Name="_tvTest" DockPanel.Dock="Left" ItemContainerStyle="{StaticResource CustomTreeViewItem}" Width="300" >
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:Data}" ItemsSource="{Binding DataItems}">
<TextBlock Text="{Binding Name}" Padding="2" />
<HierarchicalDataTemplate.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" Padding="2" />
</DataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<Button Content="Select Random TreeView Item" Click="Button_Click" Height="50" Width="200" />
</DockPanel>
</Window>
Code behind
public partial class Window1 : Window
{
private Random _random;
private List<Data> _dataItems;
public Window1()
{
InitializeComponent();
_dataItems = Init();
_tvTest.ItemsSource = _dataItems;
_random = new Random(5);
}
private List<Data> Init()
{
List<Data> dataItems = new List<Data>();
for (int i = 1; i <= 10; i++)
{
Data d1 = new Data();
d1.Name = "Data:" + i.ToString();
for (int j = 1; j <= 4; j++)
{
Data d2 = new Data();
d2.Name = "Data:" + i.ToString() + j.ToString();
d1.DataItems.Add(d2);
}
dataItems.Add(d1);
}
return dataItems;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
int index = _random.Next(0, 9);
int subIndex = _random.Next(0, 3);
if (subIndex == 0)
_dataItems[index].Selected = true;
else
_dataItems[index].DataItems[subIndex - 1].Selected = true;
}
}
In my opinion, this is a bug in WPF, but I have run into the same issue several times. I never trust the ItemContainerGenerator's Status and instead loop on separate thread as such:
private void _selectTreeViewHelper(object directory) {
TreeViewItem want = null;
bool broke = false; //probably some sort of wait timeout instead, but works for sake of example
while (true) {
Dispatcher.Invoke(new Action(delegate {
want = selectedDirectoryTreeItem.ItemContainerGenerator.ContainerFromItem(directory) as TreeViewItem;
if (want != null && want.IsLoaded) {
want.IsSelected = true;
broke = true;
}
}));
if (broke) { break; }
Thread.Sleep(100);
}
}
Then call it:
var thread = new Thread(new ParameterizedThreadStart(_selectTreeViewHelper));
thread.Start(dirWeWantSelected);
Pain I know, but it works.
Thanks for the help, I figured it out. Well it works anyway but i'm not entirely sure why it didn't before... if anyone can tell me it would be lovely... I kinda combined elements of the two responses above and then added on a little bit to make it work. For some reason, no matter what i do I cant select the TVI (TreeViewElement) if its parent wasn't already expanded, even if the parent TVI said its contents were generated and, in fact, had contents i could iterate through. My solution was thus to wait for the contents to be generated and then itrate through them and find the one i wanted. It's really weird to me that I couldn't just grab a container given its contents. Meh. My code could stand to be refactored a little bit but here it is: (works perfectly)
public void listItemClickClick(object sender, RoutedEventArgs e)
{
try
{
UserFile fil = (UserFile)(sender as ListBoxItem).DataContext;
MessageBox.Show("to do: download stuff");
return;
}
catch (InvalidCastException)
{
}
try
{
dirWeWantSelected = (Directory)(sender as ListBoxItem).DataContext;
}
catch (InvalidCastException)
{
MessageBox.Show("this should never happen");
}
selectedDirectoryTreeItem.IsExpanded = true;
TreeViewItem want = null;
try
{
want = selectedDirectoryTreeItem.ItemContainerGenerator.ContainerFromItem(dirWeWantSelected) as TreeViewItem;
}
catch
{
MessageBox.Show("weird error");
}
if (want != null)
{
want.IsSelected = true;
}
else
{
selectedDirectoryTreeItem.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
}
}
void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
if (selectedDirectoryTreeItem.ItemContainerGenerator.Status
== System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated)
{
selectedDirectoryTreeItem.ItemContainerGenerator.StatusChanged
-= ItemContainerGenerator_StatusChanged;
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input,
new Action(DelayedAction));
}
}
void DelayedAction()
{
selectedDirectoryTreeItem.Items.MoveCurrentToFirst();
Directory curr;
do
{
curr = (Directory)selectedDirectoryTreeItem.Items.CurrentItem;
if (curr.id == dirWeWantSelected.id)
{
curr.Selected = true;
return;
}
selectedDirectoryTreeItem.Items.MoveCurrentToNext();
}
while (selectedDirectoryTreeItem.Items.CurrentItem != null);
}