Modifying an ItemsPanel's Grid RowDefinitionCollection - wpf

This is a followup for ItemsControl has no children during MainWindow's constructor
Based on the answer to SO question "WPF: arranging collection items in a grid", I have the following:
<ItemsControl Name="itemsControl1" ItemsSource="{Binding MyItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid Name="theGrid" ShowGridLines="True" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="{x:Type FrameworkElement}">
<Setter Property="Grid.Row" Value="{Binding RowIndex}" />
<Setter Property="Grid.Column" Value="{Binding ColumnIndex}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
Now, I want to set the number of rows and columns of theGrid in the code behind:
theGrid.RowDefinitions.Clear();
theGrid.ColumnDefinitions.Clear();
for (uint i = 0; i < theNumberOfRows; i++)
theGrid.RowDefinitions.Add(new RowDefinition());
for (uint i = 0; i < theNumberOfCols; i++)
theGrid.ColumnDefinitions.Add(new ColumnDefinition());
As per MattHamilton's answer there, the gird is available once itemsControl1. ItemContainerGenerator.StatusChanged fires with status of GeneratorStatus.ContainersGenerated.
However, trying to modify the grid from the event handler raises an "Cannot modify 'RowDefinitionCollection' in read-only state" exception.
So, how can I set theGrid's row and column collections before the window is shown to the user?
edit: I'm modifying the Grid's properties from itemsControl1.ItemContainerGenerator.StatusChanged event handler:
if (itemsControl1.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
return;
itemsControl1.ItemContainerGenerator.StatusChanged -= ItemContainerGeneratorStatusChanged;
SetGridRowsAndColumns(InitialNumberOfRows, InitialMaxNumberOfCols);
Notice that SetGridRowsAndColumns(numberOfRows, numberOfCols) does work later, in a response to a button click.

I would use attached behavior as opposed to low level customisation for ItemsControl.
If all you need is a matrix control - you might consider using a row Grid instead of ItemsControl (that's what we've ended up with). ItemsControl is unlimitedly powerful creaturem but some time it can be a challange to squeeze a small but useful extra future through its sound design.
The changes you'll have to make following this approach are:
1. Use Dimension and bind it to to a Size you want it to be.
2. Create a custom ItemTemplate and add GridEx.Position to its root visual bound to the relvant Point property.
On those two, just give us a shout and I'll update my answer with more details.
here's the class:
public class GridEx
{
public static DependencyProperty DimensionProperty =
DependencyProperty.Register("Dimension",
typeof(Size),
typeof(Grid),
new PropertyMetadata((o, e) =>
{
GridEx.OnDimensionChanged((Grid)o, (Size)e.NewValue);
}));
public static DependencyProperty PositionProperty =
DependencyProperty.Register("Position",
typeof(Point),
typeof(UIElement),
new PropertyMetadata((o, e) =>
{
GridEx.OnPostionChanged((UIElement)o, (Point)e.NewValue);
}));
private static void OnDimensionChanged(Grid grid, Size resolution)
{
grid.RowDefinitions.Clear();
grid.ColumnDefinitions.Clear();
for (int i = 0; i < resolution.Width; i++)
{
grid.ColumnDefinitions.Add(new ColumnDefinition());
}
for (int i = 0; i < resolution.Height; i++)
{
grid.RowDefinitions.Add(new RowDefinition());
}
}
private static void OnPostionChanged(UIElement item, Point position)
{
Grid.SetColumn(item, Convert.ToInt32((position.X)));
Grid.SetRow(item, Convert.ToInt32(position.Y));
}
public static void SetDimension(Grid grid, Size dimension)
{
grid.SetValue(GridEx.DimensionProperty, dimension);
}
public static Size GetDimension(Grid grid)
{
return (Size)grid.GetValue(GridEx.DimensionProperty);
}
public static void SetPosition(UIElement item, Point position)
{
item.SetValue(GridEx.PositionProperty, position);
}
public static Point GetPosition(Grid grid)
{
return (Point)grid.GetValue(GridEx.PositionProperty);
}
}
And here's how we use it:
<Window x:Class="GridDefs.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:GridDefs"
Title="MainWindow" Height="350" Width="525">
<Grid local:GridEx.Dimension="3,3">
<Button local:GridEx.Position="0,0">A</Button>
<Button local:GridEx.Position="1,1">A</Button>
<Button local:GridEx.Position="2,2">A</Button>
</Grid>
</Window>

Here's how you can create a matrix without using ItemsControl, note, that you pertain the main thing - the ability to specify templates for the items.
Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace GridDefs
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
//
this.DataContext = this; // alterantively use RelativeSource
}
public IEnumerable<Item> MyItems
{
get
{
List<Item> items = new List<Item>(3);
items.Add(Item.Create("A1", new Point(0, 0)));
items.Add(Item.Create("B2", new Point(1, 1)));
items.Add(Item.Create("C3", new Point(2, 2)));
return items;
}
}
}
public interface IMatrixItem
{
Point Position { get; }
}
// Model, note - it has to implement INotifyPropertyChanged if
// you want to propagate its changes up to the UI
public class Item: IMatrixItem
{
public static Item Create(string text,
Point position)
{
Item item = new Item();
item.Text = text;
item.Position = position;
return item;
}
public string Text
{
get;
private set;
}
public Point Position
{
get;
private set;
}
}
public class GridEx
{
public static DependencyProperty DimensionProperty =
DependencyProperty.RegisterAttached("Dimension",
typeof(Size),
typeof(GridEx),
new PropertyMetadata(new Size(0, 0),
(o, e) =>
{
GridEx.OnDimensionChanged((Grid)o, (Size)e.NewValue);
}));
public static DependencyProperty PositionProperty =
DependencyProperty.RegisterAttached("Position",
typeof(Point),
typeof(GridEx),
new FrameworkPropertyMetadata(new Point(-1, -1),
(o, e) =>
{
GridEx.OnPostionChanged((UIElement)o, (Point)e.NewValue);
}));
public static DependencyProperty ItemStyleProperty =
DependencyProperty.RegisterAttached("ItemStyle",
typeof(Style),
typeof(GridEx));
public static DependencyProperty ItemsProperty =
DependencyProperty.RegisterAttached("Items",
typeof(IEnumerable<IMatrixItem>),
typeof(GridEx),
new PropertyMetadata((o, e) =>
{
GridEx.OnItemsChanged((Grid)o, (IEnumerable<IMatrixItem>)e.NewValue);
}));
#region "Dimension"
private static void OnDimensionChanged(Grid grid, Size resolution)
{
grid.RowDefinitions.Clear();
grid.ColumnDefinitions.Clear();
for (int i = 0; i < resolution.Width; i++)
{
grid.ColumnDefinitions.Add(new ColumnDefinition());
}
for (int i = 0; i < resolution.Height; i++)
{
grid.RowDefinitions.Add(new RowDefinition());
}
}
public static void SetDimension(Grid grid, Size dimension)
{
grid.SetValue(GridEx.DimensionProperty, dimension);
}
public static Size GetDimension(Grid grid)
{
return (Size)grid.GetValue(GridEx.DimensionProperty);
}
#endregion
#region "Position"
private static void OnPostionChanged(UIElement item, Point position)
{
item.SetValue(Grid.ColumnProperty, Convert.ToInt32(position.X));
item.SetValue(Grid.RowProperty, Convert.ToInt32(position.Y));
}
private static T GetParentOfType<T>(DependencyObject current)
where T : DependencyObject
{
for (DependencyObject parent = VisualTreeHelper.GetParent(current);
parent != null;
parent = VisualTreeHelper.GetParent(parent))
{
T result = parent as T;
if (result != null)
return result;
}
return null;
}
public static void SetPosition(UIElement item, Point position)
{
item.SetValue(GridEx.PositionProperty, position);
}
public static Point GetPosition(UIElement grid)
{
return (Point)grid.GetValue(GridEx.PositionProperty);
}
#endregion
#region "ItemStyle"
public static void SetItemStyle(Grid item, Style style)
{
item.SetValue(GridEx.ItemStyleProperty, style);
}
public static Style GetItemStyle(Grid grid)
{
return (Style)grid.GetValue(GridEx.ItemStyleProperty);
}
#endregion
#region "Items"
private static void OnItemsChanged(Grid grid, IEnumerable<IMatrixItem> items)
{
grid.Children.Clear();
// template
Style style = GetItemStyle(grid);
foreach (IMatrixItem item in items)
{
Control itemControl = new Control();
grid.Children.Add(itemControl);
itemControl.Style = style;
itemControl.DataContext = item;
}
}
public static void SetItems(Grid grid, IEnumerable<IMatrixItem> items)
{
grid.SetValue(GridEx.ItemsProperty, items);
}
public static IEnumerable<IMatrixItem> GetItems(Grid grid)
{
return (IEnumerable<IMatrixItem>)grid.GetValue(GridEx.ItemsProperty);
}
#endregion
}
}
Markup:
<Window x:Class="GridDefs.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:GridDefs"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Style TargetType="Control" x:Key="t">
<Setter Property="local:GridEx.Position" Value="{Binding Position}"></Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Button Content="{Binding Text}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid local:GridEx.Dimension="3,3"
local:GridEx.ItemStyle="{StaticResource t}"
local:GridEx.Items="{Binding MyItems}">
</Grid>
</Window>

Related

Set Name of and access individual CustomControls in ItemsControl

I am writing a programme with C#, .NET 4.6 and WPF. I would like to have a set of CustomControls arranged in a two-dimensional grid (size dynamically specified at runtime) and be able to access each CustomControl.
I did some research, found different pieces of information about the ItemsControl, and created a solution which to some extend does what I want.
Here are the relevant parts of the code, they compile and run.
XAML for CustomControl
<UserControl x:Class="TestApp.MyUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:TestApp"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Rectangle Fill="{Binding MyFill1, RelativeSource={RelativeSource FindAncestor, AncestorType=local:MyUserControl}}">
</Rectangle>
<Viewbox>
<TextBlock Text="{Binding MyText1, RelativeSource={RelativeSource FindAncestor, AncestorType=local:MyUserControl}}" >
</TextBlock>
</Viewbox>
</Grid>
</UserControl>
code-behind for CustomControl
namespace TestApp
{
public partial class MyUserControl : UserControl
{
public static readonly DependencyProperty MyText1Property =
DependencyProperty.Register("MyText1",
typeof(String), typeof(MyUserControl),
new PropertyMetadata(""));
public String MyText1
{
get { return (String)GetValue(MyText1Property); }
set { SetValue(MyText1Property, value); }
}
public static readonly DependencyProperty MyFill1Property =
DependencyProperty.Register("MyFill1",
typeof(SolidColorBrush),
typeof(MyUserControl),
new PropertyMetadata(new SolidColorBrush(Colors.Green)));
public SolidColorBrush MyFill1
{
get { return (SolidColorBrush)GetValue(MyFill1Property); }
set { SetValue(MyFill1Property, value); }
}
public MyUserControl()
{
InitializeComponent();
}
}
}
XAML for hosting MainWindow
<Window x:Class="TestApp.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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TestApp"
mc:Ignorable="d"
Name="MyMainWindow"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ItemsControl Name="MyItemsControl">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="{Binding ElementName=MyMainWindow, Path=UniformGridColumns, Mode=OneWay}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:MyUserControl MyText1="{Binding Text1}" MyFill1="{Binding Fill1}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window>
code-behind for hosting main window
namespace TestApp
{
public partial class MainWindow : Window
{
public int UniformGridColumns //number of columns of the grid
{
get { return (int)GetValue(UniformGridColumnsProperty); }
set { SetValue(UniformGridColumnsProperty, value); }
}
public static readonly DependencyProperty UniformGridColumnsProperty =
DependencyProperty.Register("UniformGridColumns", typeof(int), typeof(MainWindow),
new FrameworkPropertyMetadata((int)0));
public MainWindow()
{
InitializeComponent();
//this.DataContext = this;
Setup(13, 5); //13 columns, 5 rows
}
public void Setup(int columns, int rows) //setup the grid
{
UniformGridColumns = columns;
SingleControl[] singleControls = new SingleControl[rows*columns];
for (int i = 0; i < rows*columns; i++)
singleControls[i] = new SingleControl()
{
Text1 = (i/ columns + 1) + ", " + (i % columns + 1),
Fill1 = new SolidColorBrush((i % 2 != 0) ? Colors.Yellow : Colors.Red)
}; //example, display position in grid and fill with two different colours
MyItemsControl.ItemsSource = singleControls.ToList<SingleControl>();
}
public MyUserControl GetSingleControl(int column, int row) //access a single control
{
//some code involving VisualTreeHelper
return null;
}
private class SingleControl //helper class for setting up the grid
{
public String Text1 { get; set; }
public Brush Fill1 { get; set; }
}
}
}
The method MainWindow.Setup(int, int) fills the ItemControl with the desired number of MyCustomControls, I can label and fill them with any colour I want.
Question 1:
How can I implement GetSingleControl(int, int) that returns the MyCustomControl on the specified position? I started with a solution involving VisualTreeHelper which seems to be clumsy and unflexible.
Question 2:
How can I set Name of all MyCustomControls, e.g. something like "MyCustomControl_01_05" for the item in row 1 and column 5.
Question 3:
If questions 1 and 2 cannot be answered on the basis of my solution, what would be a more suitable approach?
Thank you!
To give an example of what both elgonzo and Andy said, you should change things to be more MVVM friendly. Once you do more research you will understand why you dont want to bother with the DependencyProperties, binding to the code behind, and manually coding all the additions of the usercontrols.
This could be made pretty or more streamlined, but i coded it to give a full example of how this could be done with MVVM. I tried to make it simple and basic, while demonstrating how to refactor your idea.
New MainWindow.xaml
<Window x:Class="TestApp.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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TestApp"
d:DataContext="{d:DesignInstance {x:Type local:MainWindowViewModel}}"
mc:Ignorable="d"
Name="MyMainWindow"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<ItemsControl Name="MyItemsControl" ItemsSource="{Binding MyList}">
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Grid.Row" Value="{Binding GridRow}" />
<Setter Property="Grid.Column" Value="{Binding GridColumn}" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="{Binding ColumnCount}" Rows="{Binding RowCount}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Rectangle Fill="{Binding Fill1}"/>
<TextBlock Text="{Binding Text1}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
New MainWindow.xaml.cs (Notice there is no extra code)
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
Add a file MainWindowViewModel.cs:
-note the MyElement could be abstracted to a viewmodel for a UserControl if you desire.
public class MyElement : INotifyPropertyChanged
{
public MyElement()
{
//some default data for design testing
Text1 = "Test";
Fill1 = new SolidColorBrush(Colors.Red);
GridColumn = 13;
GridRow = 5;
}
private string _text1;
public string Text1
{
get { return _text1; }
set{
if (value != _text1) { _text1 = value; RaisePropertyChanged(); }
}
}
private Brush _fill1;
public Brush Fill1
{
get { return _fill1; }
set
{
if (value != _fill1) { _fill1 = value; RaisePropertyChanged(); }
}
}
private int _gridRow;
public int GridRow
{
get { return _gridRow; }
set
{
if (value != _gridRow) { _gridRow = value; RaisePropertyChanged(); }
}
}
private int _gridColumn;
public int GridColumn
{
get { return _gridColumn; }
set
{
if (value != _gridColumn) { _gridColumn = value; RaisePropertyChanged(); }
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class MainWindowViewModel : INotifyPropertyChanged
{
public MainWindowViewModel() : this(13, 5) { }
public MainWindowViewModel(int columns, int rows)
{
ColumnCount = columns;
RowCount = rows;
MyList = new ObservableCollection<MyElement>();
//your original setup code
for (int i = 0; i < columns; i++)
{
for (int j = 0; j < rows; j++)
{
var vm = new MyElement
{
Text1 = (i / columns + 1) + ", " + (i % columns + 1),
Fill1 = new SolidColorBrush((i % 2 != 0) ? Colors.Yellow : Colors.Red),
GridColumn = i,
GridRow = j
};
MyList.Add(vm);
}
}
}
private int _rowCount;
public int RowCount
{
get { return _rowCount; }
set
{
if (value != _rowCount) { _rowCount = value; RaisePropertyChanged(); }
}
}
private int _columnCount;
public int ColumnCount
{
get { return _columnCount; }
set
{
if (value != _columnCount) { _columnCount = value; RaisePropertyChanged(); }
}
}
public ObservableCollection<MyElement> MyList { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
I did a more full solution where it uses INotifyPropertyChanged. I wont explain the reason for using it (in the event you are unaware), as there are much better explanations you can quickly search for.
I also made it so all the dynamic information uses Binding to make things easier to change. Now the Grid size, and item positioning are bound to your data. So it should adjust automatically as you change your "MyElement"
This should give you a good starting point for refactoring your code, and help you utilize what WPF was designed to do, as there are many mechanisms built in so you dont have to hard code UI layer manipulation (as you were in the code behind)
This also answers your Questions:
Q1 : You can now just access to the List of MyElements and change them accordingly. The UI layer should update automatically when you change anything.
Q2 : You shouldnt need to do this now, as each MyElement will keep a property for it's Grid Position. Thus you can just access that.

In WPF, is it possible to get the ItemContainerGenerator.StatusChanged event of an ItemsControl as an MVVM pattern command?

In WPF, is it possible to get the ItemContainerGenerator.StatusChanged event of an ItemsControl as an MVVM pattern command?
You can get the ItemsControl directly and register the StatusChanged event
I wonder if it is possible to implement the ItemsControl in MVVM pattern without direct access.
Try this:
public class ItemContainerGeneratorBehavior
{
public static Dictionary<ItemsControl, EventHandler> HandlersMap = new Dictionary<ItemsControl, EventHandler>();
public static Dictionary<ItemsControl, GeneratorStatus> StatusMap = new Dictionary<ItemsControl, GeneratorStatus>();
public static ICommand GetStatusCommand(DependencyObject obj)
{
return (ICommand)obj.GetValue(StatusCommandProperty);
}
public static void SetStatusCommand(DependencyObject obj, ICommand value)
{
obj.SetValue(StatusCommandProperty, value);
}
// Using a DependencyProperty as the backing store for StatusCommand. This enables animation, styling, binding, etc...
public static readonly DependencyProperty StatusCommandProperty =
DependencyProperty.RegisterAttached("StatusCommand", typeof(ICommand), typeof(ItemContainerGeneratorBehavior),
new PropertyMetadata(null, OnStatusCommandChanged));
private static void OnStatusCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var itemsControl = d as ItemsControl;
if (itemsControl == null)
return;
if (e.OldValue != null)
{
if (HandlersMap.ContainsKey(itemsControl))
{
itemsControl.ItemContainerGenerator.StatusChanged -= HandlersMap[itemsControl];
HandlersMap.Remove(itemsControl);
StatusMap.Remove(itemsControl);
}
}
if (e.NewValue != null)
{
HandlersMap[itemsControl] = (_d, _e) => ItemContainerGenerator_StatusChanged(itemsControl, _e);
StatusMap[itemsControl] = itemsControl.ItemContainerGenerator.Status;
itemsControl.ItemContainerGenerator.StatusChanged += HandlersMap[itemsControl];
}
}
private static void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
var itemsControl = sender as ItemsControl;
if (itemsControl == null)
return;
var commandHandler = GetStatusCommand(itemsControl);
var status = itemsControl.ItemContainerGenerator.Status;
var args = new StatusChangedArgs(StatusMap[itemsControl], status);
StatusMap[itemsControl] = status;
if (commandHandler.CanExecute(args))
commandHandler.Execute(args);
}
}
...which you can then use in your ItemsControl style...
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.Style>
<Style TargetType="{x:Type ItemsControl}" BasedOn="{StaticResource {x:Type ItemsControl}}">
<Setter Property="behaviors:ItemContainerGeneratorBehavior.StatusCommand" Value="{Binding RelativeSource={RelativeSource Self}, Path=DataContext.StatusChangedCommand}" />
</Style>
</ItemsControl.Style>
</ItemsControl>
...and handle in your view model...
private ICommand _StatusChangedCommand;
public ICommand StatusChangedCommand => this._StatusChangedCommand ?? (this._StatusChangedCommand = new RelayCommand<StatusChangedArgs>(OnStatusChanged));
private void OnStatusChanged(StatusChangedArgs args)
{
// do something here
}
You'll also need this:
public class StatusChangedArgs
{
public GeneratorStatus OldStatus { get; private set; }
public GeneratorStatus NewStatus { get; private set; }
public StatusChangedArgs(GeneratorStatus oldStatus, GeneratorStatus newStatus)
{
this.OldStatus = oldStatus;
this.NewStatus = newStatus;
}
public override string ToString()
{
return $"{this.OldStatus} -> {this.NewStatus}";
}
}

Binding to a Controls DependencyProperty in DataTemplate of Listbox with dynamic items does not work

Ok, since I spend one day reading articles in stackoverflow without success I must ask for help. Complete example follows below.
Short version:
I have a DoubleTextBox, which provides a DependencyProperty "Number". When I use the DoubleTextBox directly, the Binding and Validation works fine. However, when I use it in a DataTemplate of a Listbox, the Binding to Objects in an ObservableCollection does fail. ValidatesOnDataErrors works neither.
Thank you very, very much!
The XAML:
<Window x:Class="WpfApp1.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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:ViewModel x:Name="ViewModel"/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="5"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<StackPanel Margin="10">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Time (s):" Width="60"/>
<local:DoubleTextBox Width="60"
Number="{Binding Time, UpdateSourceTrigger=PropertyChanged,
ValidatesOnDataErrors=True}" Decimals="2"/>
</StackPanel>
</StackPanel>
</Grid>
<Grid Grid.Column="1" Background="SteelBlue"/>
<Grid Grid.Column="2">
<Grid.Resources>
<DataTemplate x:Key="MethodProgramViewTemplate">
<Grid KeyboardNavigation.TabNavigation="Local">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="60"/>
<ColumnDefinition Width="60"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Flow"/>
<local:DoubleTextBox Grid.Column="1" TabIndex="0" Number="{Binding Flow, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" Decimals="2" />
</Grid>
</DataTemplate>
</Grid.Resources>
<ListBox x:Name="PumpProgramListBox"
KeyboardNavigation.TabNavigation="Local"
ItemsSource="{Binding Path=MethodProgramView}"
ItemTemplate="{StaticResource MethodProgramViewTemplate}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Grid>
</Grid>
</Window>
The ViewModel:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Windows.Data;
namespace WpfApp1
{
public class ViewModel : INotifyPropertyChanged, IDataErrorInfo
{
private double time = 0.0;
public double Time
{
get => this.time;
set
{
if (this.time != value)
{
System.Diagnostics.Debug.WriteLine(String.Format("ViewModel Time - from {0} to {1}", this.time, value));
this.time = value;
RaisePropertyChanged(nameof(Time));
}
}
}
public ObservableCollection<Method> methodProgram { get; private set; } = new ObservableCollection<Method>();
private CollectionViewSource methodProgramView;
public ICollectionView MethodProgramView
{
get
{
if (methodProgramView == null)
{
methodProgramView = new CollectionViewSource();
methodProgramView.Source = methodProgram;
}
return methodProgramView.View;
}
}
public ViewModel()
{
this.Time = 1.5;
for (int i = 1; i <= 3; i++)
{
this.methodProgram.Add(new Method() { Flow = i });
}
}
public event PropertyChangedEventHandler PropertyChanged;
internal void RaisePropertyChanged(string propName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
public string Error => "";
public string this[string columnName]
{
get
{
string result = null;
switch (columnName)
{
case nameof(Time):
result = this.Time < 0 ? "Time must be positive" : null;
break;
default:
break;
}
return result;
}
}
}
}
The "Method" Object:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
namespace WpfApp1
{
public class Method : INotifyPropertyChanged, IDataErrorInfo
{
private double flow = 0.0;
public double Flow
{
get => this.flow;
set
{
if (this.flow != value)
{
System.Diagnostics.Debug.WriteLine(String.Format("Method Flow - from {0} to {1}", this.flow, value));
this.flow = value;
RaisePropertyChanged(nameof(Flow));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
internal void RaisePropertyChanged(string propName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
public string Error => "";
public string this[string columnName]
{
get
{
string result = null;
switch (columnName)
{
case nameof(Flow):
result = this.Flow < 0 ? "Flow must be positive" : null;
break;
default:
break;
}
return result;
}
}
}
}
The DoubleTextBox:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
namespace WpfApp1
{
public class DoubleTextBox : TextBox
{
#region Properties
public double Number
{
get { return (double)this.GetValue(NumberProperty); }
set { this.SetValue(NumberProperty, value); }
}
public static readonly DependencyProperty NumberProperty = DependencyProperty.Register(
nameof(Number), typeof(double), typeof(DoubleTextBox),
new FrameworkPropertyMetadata(3.3d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
new PropertyChangedCallback(NumberProperty_PropertyChanged))
);
public int Decimals
{
get { return (int)this.GetValue(DecimalsProperty); }
set { this.SetValue(DecimalsProperty, value); }
}
public static readonly DependencyProperty DecimalsProperty = DependencyProperty.Register(
nameof(Decimals), typeof(int), typeof(DoubleTextBox),
new FrameworkPropertyMetadata((int)2, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
new PropertyChangedCallback(DecimalsProperty_PropertyChanged))
);
#endregion Properties
#region Constructor
public DoubleTextBox()
{
this.TextChanged += DoubleTextBox_TextChanged;
this.LostFocus += DoubleTextBox_LostFocus;
this.PreviewTextInput += DoubleTextBox_PreviewTextInput;
this.RefreshText();
}
#endregion Constructor
#region Methods
private static void NumberProperty_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
System.Diagnostics.Debug.WriteLine(String.Format("NumberProperty_PropertyChanged - from {0} to {1}", e.OldValue, e.NewValue));
(d as DoubleTextBox).RefreshText();
}
private static void DecimalsProperty_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
(d as DoubleTextBox).RefreshText();
}
private void RefreshText()
{
System.Diagnostics.Debug.WriteLine(String.Format("DoubleTextBox - DataContext: {0}", this.DataContext));
Text = Number.ToString("N" + this.Decimals.ToString());
}
private void DoubleTextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
e.Handled = !IsTextAllowed(e.Text);
}
private static bool IsTextAllowed(string text)
{
string CultureName = System.Threading.Thread.CurrentThread.CurrentCulture.Name;
System.Globalization.CultureInfo ci = new System.Globalization.CultureInfo(CultureName);
char DecimalSeparator = ci.NumberFormat.NumberDecimalSeparator[0];
Regex regex = new Regex(String.Format("([-+0-9]+[{0}][0-9]+)|([-+0-9{0}])", DecimalSeparator)); //regex that matches only numbers
bool match = regex.IsMatch(text);
return regex.IsMatch(text);
}
void DoubleTextBox_LostFocus(object sender, System.Windows.RoutedEventArgs e)
{
RefreshText();
}
void DoubleTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
double number;
if (string.IsNullOrEmpty(Text))
{
Number = 0;
}
else if (double.TryParse(Text, out number))
{
Number = Double.Parse(number.ToString("N" + this.Decimals.ToString()));
}
else
{
BindingExpression bex = GetBindingExpression(NumberProperty);
if (bex != null)
{
ValidationError validationError = new ValidationError(new ExceptionValidationRule(), bex);
validationError.ErrorContent = "Number is not valid";
Validation.MarkInvalid(bex, validationError);
}
}
}
#endregion Methods
}
}
The call of RefreshText() in the constructor of DoubleTextBox has to be removed. Still don't know why, but it works.
Thanks to #user1481065 and this issue: WPF Binding to DependencyProperty of UserControl in DataTemplate is not working

ListBox Map J and K Keys to Up/Down Arrow Keys

I have a ListBox and I simply want to bind the J and K keys to whatever commands the up and down arrow keys are bound to. The up and down arrow keys in a WPF listbox typically change the selected item to the previous/next item. I thought something like this should work:
<ListBox.InputBindings>
<KeyBinding Key="J" Command="ScrollBar.LineDownCommand" />
<KeyBinding Key="K" Command="ScrollBar.LineUpCommand" />
</ListBox.InputBindings>
I'm probably being too simplistic here.
You can use your DependencyClass on the commands. Define the commands in ListBox.InputBindings:
XAML
<ListBox Name="SampleListBox" Width="200" Height="200" KeyboardNavigation.DirectionalNavigation="Cycle" SelectedIndex="{Binding MySelectedIndex}">
<ListBox.InputBindings>
<KeyBinding Command="{Binding NextCommand}" Gesture="CTRL+J" />
<KeyBinding Command="{Binding PrevCommand}" Gesture="CTRL+K" />
</ListBox.InputBindings>
<ListBoxItem>Sample 1</ListBoxItem>
<ListBoxItem>Sample 2</ListBoxItem>
<ListBoxItem>Sample 3</ListBoxItem>
<ListBoxItem>Sample 4</ListBoxItem>
</ListBox>
Code behind
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// Set your data
this.DataContext = new MainWindowViewModel();
// Set focus
SampleListBox.Focus();
}
}
/// <summary>
/// Class with commands
/// </summary>
public class MainWindowViewModel : DependencyObject
{
public ICommand NextCommand
{
get;
set;
}
public ICommand PrevCommand
{
get;
set;
}
public int MySelectedIndex
{
get
{
return (int)GetValue(MySelectedIndexProperty);
}
set
{
SetValue(MySelectedIndexProperty, value);
}
}
public static readonly DependencyProperty MySelectedIndexProperty =
DependencyProperty.Register("MySelectedIndex", typeof(int), typeof(MainWindowViewModel), new UIPropertyMetadata(0));
public MainWindowViewModel()
{
MySelectedIndex = 0;
NextCommand = new SimpleCommand(SetNext);
PrevCommand = new SimpleCommand(SetPrev);
}
private void SetNext()
{
MySelectedIndex += 1;
}
private void SetPrev()
{
if (MySelectedIndex > 0)
{
MySelectedIndex -= 1;
}
}
}
public class SimpleCommand : ICommand
{
private Action _action;
public SimpleCommand(Action p_action)
{
_action = p_action;
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
if (_action != null)
{
_action();
}
}
}
In the class contains two ICommand's: NextCommand and PrevCommand. Also there is a DependencyProperty MySelectedIndex, which contains the current index of the item. In SimpleCommand always return true.
This is just an example that still need to check the total number of Items ListBox. Or instead of increasing the SelectedIndex, use ScrollViewer logic.
Extension
Example with ScrollViewer:
To scroll through the items in the ListBox, you must first have access to it. Below is the corresponding function:
public static DependencyObject GetScrollViewer(DependencyObject Object)
{
if (Object is ScrollViewer)
{
return Object;
}
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(Object); i++)
{
var child = VisualTreeHelper.GetChild(Object, i);
var result = GetScrollViewer(child);
if (result == null)
{
continue;
}
else
{
return result;
}
}
return null;
}
Simple function scrolling:
private void OnScrollDown(object sender, RoutedEventArgs e)
{
if (MyListBox.Items.Count > 0)
{
// Get ScrollViewer from ListBox
ScrollViewer scrollViewer = GetScrollViewer(MyListBox) as ScrollViewer;
if (scrollViewer != null)
{
// Increment offset - scrolling Down, sub - scrolling Up
scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset + ScrollListBoxOffset);
}
}
}

Clear wpf listbox selection using button in control template and no codebehind

I want to create a Style for a WPF ListBox that includes a Button in the ControlTemplate that the user can click on and it clears the ListBox selection.
I dont want to use codebehind so that this Style can be applied to any ListBox.
I have tried using EventTriggers and Storyboards and it has proved problematic as it only works first time and stopping the Storyboard sets the previous selection back.
I know I could use a user control but I want to know if it is possible to achieve this using only a Style.
It is not possible to achieve this using XAML and only the classes provided by the .NET framework. However you can still produce a reusable solution by defining a new command (call it ClearSelectionCommand) and a new attached property (call it ClearSelectionOnCommand).
Then you can incorporate those elements into your style.
Example:
public class SelectorBehavior
{
public static RoutedCommand
ClearSelectionCommand =
new RoutedCommand(
"ClearSelectionCommand",
typeof(SelectorBehavior));
public static bool GetClearSelectionOnCommand(DependencyObject obj)
{
return (bool)obj.GetValue(ClearSelectionOnCommandProperty);
}
public static void SetClearSelectionOnCommand(
DependencyObject obj,
bool value)
{
obj.SetValue(ClearSelectionOnCommandProperty, value);
}
public static readonly DependencyProperty ClearSelectionOnCommandProperty =
DependencyProperty.RegisterAttached(
"ClearSelectionOnCommand",
typeof(bool),
typeof(SelectorBehavior),
new UIPropertyMetadata(false, OnClearSelectionOnCommandChanged));
public static void OnClearSelectionOnCommandChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
Selector selector = d as Selector;
if (selector == null) return;
bool nv = (bool)e.NewValue, ov = (bool)e.OldValue;
if (nv == ov) return;
if (nv)
{
selector.CommandBindings.Add(
new CommandBinding(
ClearSelectionCommand,
ClearSelectionCommand_Executed,
ClearSelectionCommand_CanExecute));
}
else
{
var cmd = selector
.CommandBindings
.Cast<CommandBinding>()
.SingleOrDefault(x =>
x.Command == ClearSelectionCommand);
if (cmd != null)
selector.CommandBindings.Remove(cmd);
}
}
public static void ClearSelectionCommand_Executed(
object sender,
ExecutedRoutedEventArgs e)
{
Selector selector = (Selector)sender;
selector.SelectedIndex = -1;
}
public static void ClearSelectionCommand_CanExecute(
object sender,
CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
}
Example usage - XAML:
<Window x:Class="ClearSelectionBehaviorLibrary.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ClearSelectionBehaviorLibrary"
Title="Window1" Height="300" Width="300">
<Window.Resources>
<Style x:Key="MyStyle" TargetType="Selector">
<Setter
Property="local:SelectorBehavior.ClearSelectionOnCommand"
Value="True"/>
</Style>
</Window.Resources>
<Grid>
<DockPanel>
<Button
DockPanel.Dock="Bottom"
Content="Clear"
Command="{x:Static local:SelectorBehavior.ClearSelectionCommand}"
CommandTarget="{Binding ElementName=TheListBox}"/>
<ListBox
Name="TheListBox"
ItemsSource="{Binding MyData}"
Style="{StaticResource MyStyle}"/>
</DockPanel>
</Grid>
</Window>
Example usage - Code Behind:
public partial class Window1 : Window
{
public List<string> MyData { get; set; }
public Window1()
{
MyData = new List<string>
{
"aa","bb","cc","dd","ee"
};
InitializeComponent();
DataContext = this;
}
}

Resources