WPF Nested ItemsControl with custom Panel & ItemContainer - c#

I am creating a rudimentary Gantt chart as a visual representation of a schedule of events. To do this, I have an ItemsControl to render schedule line items in a StackPanel. Within that "parent" ItemsControl, I have another ItemsControl to render the Gantt chart view - basically just shapes. This looks like the following:
<ItemsControl ItemsSource="{Binding ScheduleLines}"
Grid.Row="1"
Height="100">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}"
Grid.Column="0"/>
<ItemsControl ItemsSource="{Binding Events}"
Grid.Column="1">
<ItemsPanelTemplate>
<components:ScheduleRowPanel/>
</ItemsPanelTemplate>
<ItemContainerTemplate>
<scheduleitems:ScheduleEventElement EventDate="{Binding EventDate}"
Status="{Binding Status}"/>
</ItemContainerTemplate>
</ItemsControl>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
The datacontext for this view has an ObservableCollection called ScheduleLines. Each item of ScheduleLines has another observable collection of Events.
Inside the child ItemsControl, I have a custom panel that arranges the events:
public class ScheduleRowPanel : Panel
{
//Scale and MinDate are dependency properties on this custom panel
protected override Size ArrangeOverride(Size finalSize)
{
foreach(UIElement child in Children)
{
ArrangeChild(child, MinDate, Scale, finalSize.Height);
}
return finalSize;
}
private void ArrangeChild(UIElement child, DateOnly minDate, double scale, double panelHeight)
{
if (child.GetType() == typeof(ScheduleEventElement))
{
ScheduleEventElement eventElement = (ScheduleEventElement)child;
DateOnly eventDate = eventElement.EventDate;
double xoffset = scale * (eventDate.DayNumber - minDate.DayNumber);
double yoffset = panelHeight / 2;
child.Arrange(new Rect(xoffset, yoffset, 50, 50));
}
else
{
throw new InvalidOperationException("Have not implemented any other type of schedule entity");
}
}
}
The panel arranges events from the following, that are rotated rectangles with a fill:
public class ScheduleEventElement : FrameworkElement
{
public DateOnly EventDate
{
get { return (DateOnly)GetValue(EventDateProperty); }
set { SetValue(EventDateProperty, value); }
}
// Using a DependencyProperty as the backing store for EventDate. This enables animation, styling, binding, etc...
public static readonly DependencyProperty EventDateProperty =
DependencyProperty.Register("EventDate", typeof(DateOnly), typeof(ScheduleEventElement), new PropertyMetadata(DateOnly.FromDayNumber(1)));
public EventStatus Status
{
get { return (EventStatus)GetValue(StatusProperty); }
set { SetValue(StatusProperty, value); }
}
// Using a DependencyProperty as the backing store for Status. This enables animation, styling, binding, etc...
public static readonly DependencyProperty StatusProperty =
DependencyProperty.Register("Status", typeof(EventStatus), typeof(ScheduleEventElement), new PropertyMetadata(EventStatus.Scheduled));
protected override void OnRender(DrawingContext drawingContext)
{
RectangleGeometry rectangle = new RectangleGeometry();
SolidColorBrush fill = new SolidColorBrush();
rectangle.Rect = new Rect(0, 0, 10, 0);
RotateTransform rotate = new RotateTransform();
rotate.CenterX = 0.5;
rotate.CenterY = 0.5;
rotate.Angle = 45;
rectangle.Transform = rotate;
switch (Status)
{
case EventStatus.Scheduled:
fill.Color = (Color)ColorConverter.ConvertFromString("#262626");
break;
case EventStatus.Early:
fill.Color = (Color)ColorConverter.ConvertFromString("#009d9a");
break;
case EventStatus.Late:
fill.Color = (Color)ColorConverter.ConvertFromString("#6929c4");
break;
}
drawingContext.DrawGeometry(fill, null, rectangle);
}
}
I am getting the following error when running the nested ItemControl:
I am understanding this as I need to modify the inner ItemControl to instead of having ItemsSource = {Binding Events} to have <ItemsControl> <ItemsControl.ItemsSource/>, but I am not sure how to specify the items source using that syntax. Is my understanding correct? If so, what would be the correct syntax for this?
Further, do I have a correct implementation of the ItemsContainerTemplate? Or should this be in the ItemsTemplate?
Thanks

you can't just write <ItemsPanelTemplate> and <ItemContainerTemplate> inside <ItemsControl> tag, you need to write them inside relveant property tags: <ItemsControl.ItemsPanel> and <ItemsControl.ItemTemplate>.
<ItemsControl ItemsSource="{Binding Events}"
Grid.Column="1">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<components:ScheduleRowPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<scheduleitems:ScheduleEventElement EventDate="{Binding EventDate}"
Status="{Binding Status}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

Related

Why is data binding to canvas not updating UI

I am trying to data bind a collection of lines, and perform a sort function on them and update the UI once the sort has been completed (would like to show the differences in sort algorithms).
I have a basic WPF application which consists of an ItemsControl which is bound to a collection of objects. These objects are bound correctly when the screen is first rendered, however once the sort operation has been completed, the underlying list has been sorted correctly, but the UI has not been redrawn?
Here is my XAML
<Grid>
<Button Content="Sort" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="12" MinWidth="80" Click="Button_Click"/>
<ItemsControl x:Name="mainControl" ItemsSource="{Binding Values}" ItemTemplate="{StaticResource LineDataTemplate}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter" />
</ItemsControl.ItemContainerStyle>
</ItemsControl>
</Grid>
there is a xaml data template
<DataTemplate x:Key="LineDataTemplate">
<Line X1="{Binding X1}" Y1="{Binding Y1}"
X2="{Binding X2}" Y2="{Binding Y2}"
Stroke="DarkGray" StrokeThickness="3"/>
</DataTemplate>
The main data context contains a list of this Line object
public class Line
{
public int X1 { get; set; }
public int Y1 { get; set; }
public int X2 { get; set; }
public int Y2 { get; set; }
}
And when the datacontext is initialised I create some random lines
private void RandomiseLines()
{
var rnd = new Random();
var startingPoint = 2;
Values = new List<Line>();
for (int i = 0; i < 3; i++)
{
Values.Add(new Line() { X1 = startingPoint, Y1 = 420, X2 = startingPoint, Y2 = (420 - rnd.Next(1, 300)) });
startingPoint += 4;
}
}
Then I have a button on the UI which calls through and (for now) calls a basic sort using linq
Values = Values.OrderBy(x => x.Y2).ToList();
The data context, where this list is held implements the INotifiedProperty changed interface, and once the list is sorted I make a call to the Property changed event. Although the underlying list get sorted the UI does not seem to be redrawing, I have tried using an ObservableCollection and wrapping in Dispatcher but I do not seem to have any binding errors or exceptions being thrown. Can anyone please explain why this does not get updated?
Edit: Added expected result
The expected result would be the ItemsControl redrawing itself and the lines would be in the new sorted order
You better use Rectangle instead of Line, because it doesn't rely on coordinates for positioning. You just give them a shared Width but a variable Hight. The ItemsPanel should be a StackPanel with the StackPanel.Orientation set to Horizontal. The Value collection must be a ObservableCollection<double>. Then it should behave as expected.
This way the order of the bars will reflect the order of the collection.
The main view
<StackPanel>
<Button Content="Sort"
Click="Button_Click" />
<ItemsControl x:Name="mainControl"
ItemsSource="{Binding Values}"
ItemTemplate="{DynamicResource LineDataTemplate}">
<ItemsControl.Resources>
<DataTemplate x:Key="LineDataTemplate" DataType="system:Double">
<Rectangle Width="5"
Height="{Binding}"
VerticalAlignment="Bottom"
Fill="DarkGray"
Margin="0,0,3,0" />
</DataTemplate>
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</StackPanel>
The Button.Click event handler
private void Button_Click(object sender, RoutedEventArgs e)
{
var viewModel = this.DataContext as TestViewModel;
var orderedValues = viewModel.Values.OrderBy(value => value).ToList();
viewModel.Values = new ObservableCollection<double>(orderedValues);
}
The view model
private void RandomiseLines()
{
var rnd = new Random();
Values = new ObservableCollection<double>();
for (int i = 0; i < 3; i++)
{
Values.Add(rnd.Next(1, 300));
}
}

How to implement a WrapPanel with a header and footer

I am trying to implement a custom control that acts similarly to a standard WrapPanel, but that allows you to specify a header and footer. Visually, this is what I am trying to accomplish:
I have created a custom control that seems to leave room for the header and footer items, but I am unable to get them to visually appear. This is my first attempt at any sort of custom control, so any help or input is appreciated!
C#
using System;
using System.Windows;
using System.Windows.Controls;
namespace MyProject.Extras
{
public class HeaderedFooteredPanel : Panel
{
public FrameworkElement Header
{
get { return (FrameworkElement) GetValue(HeaderProperty); }
set { SetValue(HeaderProperty, value); }
}
public FrameworkElement Footer
{
get { return (FrameworkElement)GetValue(FooterProperty); }
set { SetValue(FooterProperty, value); }
}
public static DependencyProperty HeaderProperty = DependencyProperty.Register(
nameof(Header),
typeof(FrameworkElement),
typeof(HeaderedFooteredPanel),
new PropertyMetadata((object)null));
public static DependencyProperty FooterProperty = DependencyProperty.Register(
nameof(Footer),
typeof(FrameworkElement),
typeof(HeaderedFooteredPanel),
new PropertyMetadata((object)null));
protected override Size MeasureOverride(Size constraint)
{
double x = 0.0;
double y = 0.0;
double largestY = 0.0;
double largestX = 0.0;
var measure = new Action<FrameworkElement>(element =>
{
element.Measure(constraint);
if (x > 0 && // Not the first item on this row
(x + element.DesiredSize.Width > constraint.Width) && // We are too wide to fit on this row
((largestY + element.DesiredSize.Height) <= MaxHeight)) // We have enough room for this on the next row
{
y = largestY;
x = element.DesiredSize.Width;
}
else
{
/* 1) Always place the first item on a row even if width doesn't allow it
* otherwise:
* 2) Keep placing on this row until we reach our width constraint
* otherwise:
* 3) Keep placing on this row if the max height is reached */
x += element.DesiredSize.Width;
}
largestY = Math.Max(largestY, y + element.DesiredSize.Height);
largestX = Math.Max(largestX, x);
});
measure(Header);
foreach (FrameworkElement child in InternalChildren)
{
measure(child);
}
measure(Footer);
return new Size(largestX, largestY);
}
protected override Size ArrangeOverride(Size finalSize)
{
double x = 0.0;
double y = 0.0;
double largestY = 0.0;
double largestX = 0.0;
var arrange = new Action<FrameworkElement>(element =>
{
if (x > 0 && // Not the first item on this row
(x + element.DesiredSize.Width > finalSize.Width) && // We are too wide to fit on this row
((largestY + element.DesiredSize.Height) <= MaxHeight)) // We have enough room for this on the next row
{
y = largestY;
element.Arrange(new Rect(new Point(0.0, y), element.DesiredSize));
x = element.DesiredSize.Width;
}
else
{
/* 1) Always place the first item on a row even if width doesn't allow it
* otherwise:
* 2) Keep placing on this row until we reach our width constraint
* otherwise:
* 3) Keep placing on this row if the max height is reached */
element.Arrange(new Rect(new Point(x, y), element.DesiredSize));
x += element.DesiredSize.Width;
}
largestY = Math.Max(largestY, y + element.DesiredSize.Height);
largestX = Math.Max(largestX, x);
});
arrange(Header);
foreach (FrameworkElement child in InternalChildren)
{
arrange(child);
}
arrange(Footer);
return new Size(largestX, largestY);
}
}
}
Usage in XAML:
<ItemsControl ItemsSource="{Binding SomeItems}" ItemTemplate="{StaticResource SomeTemplate}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<extras:HeaderedFooteredPanel>
<extras:HeaderedFooteredPanel.Header>
<TextBlock Text="Header" />
</extras:HeaderedFooteredPanel.Header>
<extras:HeaderedFooteredPanel.Footer>
<TextBlock Text="Footer" />
</extras:HeaderedFooteredPanel.Footer>
</extras:HeaderedFooteredPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
You write in the comments:
The DrawingContext supplied to the OnRender() method only seems to support very basic rendering commands. Surely you don't have to re-write the rendering code for a standard WPF control, but I am not seeing a way to draw them on my own
If by "basic" you mean you are restricted to only DrawingContext operations, then yes. That's exactly what it's for. That's actually the drawing API for WPF. At a higher level, you are dealing with visuals and framework elements, which hide the actual drawing activity. But to override the way such objects are drawn, will require diving down into that level of drawing, replacing it or supplementing it as necessary.
One significant difficulty that would probably arise (besides the more fundamental difficulty of dealing with drawing at that level) is that at that level, there is no such thing as data templates, and no way to access the rendering behaviors of other elements. You have to draw everything from scratch. This would wind up negating a big part of what makes WPF so useful: convenient and powerful control over the exact on-screen representation of data through the use of built-in controls and the properties that give you control over their appearance.
I have only rarely found that a custom Control sub-class is really needed. The only time this comes up is when you need to have complete control over the entire rendering process, to draw something that is simply not possible any other way, or to provide the required performance (at the expense of convenience). Much more often, nearly all of the time even, what you want to do is leverage the existing controls and get them to do all the heavy lifting for you.
In this particular case, I think the key to solving your problem is a type called CompositeCollection. Just like it sounds, it allows you build up a collection as a composite of other objects, including other collections. With this, you can combine your header and footer data into a single collection that can be displayed by an ItemsControl.
In some cases, just creating that collection and using it directly with an ItemsControl object might be sufficient for your needs. But if you want a whole, reusable user-defined control that understands the idea of a header and footer, you can wrap the ItemsControl in a UserControl object that exposes the properties you need, including a Header and Footer property. Here is an example of what that might look like:
XAML:
<UserControl x:Class="TestSO43008469HeaderFooterWrapPanel.HeaderFooterWrapPanel"
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:TestSO43008469HeaderFooterWrapPanel"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<ItemsControl x:Name="wrapPanel1">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</UserControl>
C#:
public partial class HeaderFooterWrapPanel : UserControl
{
private const int _kheaderIndex = 0;
private const int _kfooterIndex = 2;
private readonly CompositeCollection _composedCollection = new CompositeCollection();
private readonly CollectionContainer _container = new CollectionContainer();
public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register(
"Header", typeof(string), typeof(HeaderFooterWrapPanel),
new PropertyMetadata((o, e) => _OnHeaderFooterPropertyChanged(o, e, _kheaderIndex)));
public static readonly DependencyProperty FooterProperty = DependencyProperty.Register(
"Footer", typeof(string), typeof(HeaderFooterWrapPanel),
new PropertyMetadata((o, e) => _OnHeaderFooterPropertyChanged(o, e, _kfooterIndex)));
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
"ItemsSource", typeof(IEnumerable), typeof(HeaderFooterWrapPanel),
new PropertyMetadata(_OnItemsSourceChanged));
private static void _OnHeaderFooterPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e, int index)
{
HeaderFooterWrapPanel panel = (HeaderFooterWrapPanel)d;
panel._composedCollection[index] = e.NewValue;
}
private static void _OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
HeaderFooterWrapPanel panel = (HeaderFooterWrapPanel)d;
panel._container.Collection = panel.ItemsSource;
}
public string Header
{
get { return (string)GetValue(HeaderProperty); }
set { SetValue(HeaderProperty, value); }
}
public string Footer
{
get { return (string)GetValue(FooterProperty); }
set { SetValue(FooterProperty, value); }
}
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public HeaderFooterWrapPanel()
{
InitializeComponent();
_container.Collection = ItemsSource;
_composedCollection.Add(Header);
_composedCollection.Add(_container);
_composedCollection.Add(Footer);
wrapPanel1.ItemsSource = _composedCollection;
}
}
Noting, of course, that doing it that way, you would need to "forward" all of the various control properties that you want to be able to set, from the UserControl object to the ItemsPanel. Some, like Background, you can probably just set on the UserControl and have the desired effect, but others are specifically applicable to the ItemsControl, like ItemTemplate, ItemTemplateSelector, etc. You'll have to figure out which those are, and bind the properties, with the source being the UserControl and the target the ItemsControl inside, declaring as dependency properties in your UserControl class any that aren't already part of the UserControl type.
Here's a little sample program that shows how the above could be used:
XAML:
<Window x:Class="TestSO43008469HeaderFooterWrapPanel.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:l="clr-namespace:TestSO43008469HeaderFooterWrapPanel"
xmlns:s="clr-namespace:System;assembly=mscorlib"
DataContext="{Binding RelativeSource={x:Static RelativeSource.Self}}"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row="0">
<TextBlock Text="Header: "/>
<TextBox Text="{Binding Header, ElementName=headerFooterWrapPanel1, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Grid.Row="1">
<TextBlock Text="Footer: "/>
<TextBox Text="{Binding Footer, ElementName=headerFooterWrapPanel1, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
<Button Content="Random List Change" Click="Button_Click" HorizontalAlignment="Left" Grid.Row="2"/>
<l:HeaderFooterWrapPanel x:Name="headerFooterWrapPanel1" ItemsSource="{Binding Items}"
Header="Header Item" Footer="Footer Item" Grid.Row="3">
<l:HeaderFooterWrapPanel.Resources>
<DataTemplate DataType="{x:Type s:String}">
<Border BorderBrush="Black" BorderThickness="1">
<TextBlock Text="{Binding}" FontSize="16"/>
</Border>
</DataTemplate>
</l:HeaderFooterWrapPanel.Resources>
</l:HeaderFooterWrapPanel>
</Grid>
</Window>
For the purpose of illustration, I've set the Window.DataContext property to the Window object itself. This isn't normally a good idea — it's better to have a proper view model to use as the data context — but for a simple program like this, it's fine. Similarly, the Header and Footer properties would normally be bound to some view model property instead of just tying one framework element's property to another.
C#:
public partial class MainWindow : Window
{
public ObservableCollection<string> Items { get; } = new ObservableCollection<string>();
public MainWindow()
{
InitializeComponent();
Items.Add("Item #1");
Items.Add("Item #2");
Items.Add("Item #3");
}
private static readonly Random _random = new Random();
private void Button_Click(object sender, RoutedEventArgs e)
{
switch (Items.Count > 0 ? _random.Next(2) : 0)
{
case 0: // add
Items.Insert(_random.Next(Items.Count + 1), $"Item #{_random.Next()}");
break;
case 1: // remove
Items.RemoveAt(_random.Next(Items.Count));
break;
}
}
}

Dynamically sizing 2 dimensional Grid

I am working on creating the card game "Memory" In WPF. I am having trouble on the UI side of it. I have set it up so that when the user selects a difficulty it dynamically sets the size of the deck (4x4 for easy, this is what we will be working/talking about for proof of concept). How do I allow for the dynamic change of grid when selecting different difficulties?
This is where you set the difficulty (All the cards are for testing purposes..)
private void SetDifficulty(Difficulty difficulty) {
//Clearing CardList
CardList.Clear();
//Switching on the diff
switch (difficulty) {
case Difficulty.Easy:
CardList = new ObservableCollection<Card>{
new Card {
Image = Resources.Bowser
},
new Card(),
new Card(),
new Card(),
new Card(),
new Card(),
new Card(),
new Card(),
new Card(),
new Card(),
new Card(),
new Card(),
new Card(),
new Card(),
new Card(),
new Card()
};
break;
case Difficulty.Medium:
break;
case Difficulty.Hard:
break;
default:
throw new ArgumentOutOfRangeException(nameof(difficulty), difficulty, null);
}
}
XAML:
<Window x:Class="MemoryGame.Views.MainView"
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:MemoryGame.Views"
xmlns:viewModels="clr-namespace:MemoryGame.ViewModels"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance viewModels:MemoryGameViewModel}"
Title="MainView" Height="300" Width="300">
<Grid ShowGridLines="True">
<ListBox ItemsSource="{Binding Path=CardList}">
<ListBox.ItemTemplate>
<DataTemplate DataType="viewModels:Card">
<StackPanel Orientation="Horizontal" Width="50" Height="50" >
<!--<Image Source="/Pictures/Luigi.jpg"></Image>-->
<Button Content="{Binding Image, UpdateSourceTrigger=PropertyChanged}" Margin="5" Height="50" Width="50">
</Button>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
I would prefer using ItemsControl over ListBox for the collection in the XAML. I would also BIND the width of the collection and add wrapper panel that allows for wrapping objects:
<ItemsControl ItemsSource="{Binding Path=CardList} Width="{Binding CollWidth}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="viewModels:Card">
<StackPanel Orientation="Horizontal" Width="50" Height="50" >
<!--<Image Source="/Pictures/Luigi.jpg"></Image>-->
<Button Content="{Binding Image, UpdateSourceTrigger=PropertyChanged}" Margin="5" Height="50" Width="50">
</Button>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<!-- A WrapPanel ensures the items wrap to the next line -->
<!-- when it runs out of room in the collection dimensions -->
<WrapPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Don't forget to add CollWidth property:
private int _collWidth;
public int CollWidth {
get { return _collWidth; }
set {
_collWidth = value;
OnPropertyChanged("CollWidth");
}
}
Now you can easily modify your SetDificulty method:
private void SetDifficulty(Difficulty difficulty) {
// this auto clears everything
CardList = new ObservableCollection<Card>();
//Switching on the diff
switch (difficulty) {
case Difficulty.Easy:
// set the width in each level of difficulty to allow wrapper to make nice looking grid
CollWidth = 200; // (button width is 50) * 4 buttons = 200
for(int i=0;i<16;i++)
CardList.Add(new Card()); // or whatever constructor
break;
case Difficulty.Medium:
CollWidth = 250; // (button width is 50) * 5 buttons = 250
for(int i=0;i<25;i++)
CardList.Add(new Card()); // or whatever constructor
break;
case Difficulty.Hard:
CollWidth = 300; // (button width is 50) * 6 buttons = 300
for(int i=0;i<36;i++)
CardList.Add(new Card()); // or whatever constructor
break;
default:
throw new ArgumentOutOfRangeException(nameof(difficulty), difficulty, null);
}
}
You do not have to worry about the Height of the collection. The added <WrapPanel> will take care of that for you. You only specify the CollWidth and the wrap panel will put CollWidth/50 buttons in each row
You could use a UniformGrid as ItemsPanel of your ListBox, and bind its Columns or Rows property to a property in your view model:
<ListBox ItemsSource="{Binding CardList}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="{Binding GridSize}"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Image}" Margin="5" Height="50" Width="50"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
As a note, setting UpdateSourceTrigger=PropertyChanged on the Content Binding of the Button makes no sense. It only has an effect on Bindings that actually trigger an update of their source property, i.e. TwoWay or OneWayToSource Bindings.
It is also bad practice to create UI elements in code behind, like the Image property of your Card class. Better declare a property of type ImageSource, and bind an Image control's Source property in XAML:
public class Card
{
public ImageSource Image { get; set; }
...
}
...
var card = new Card
{
Image = new BitmapeImage(new Uri("pack://application:,,,/Picures/Luigi.jpg"))
};
XAML:
<ListBox.ItemTemplate>
<DataTemplate>
<Button Margin="5" Height="50" Width="50">
<Image Source="{Binding Image}"/>
</Button>
</DataTemplate>
</ListBox.ItemTemplate>

WPF: ListBox item show from bottom to top

I think title is not clear
I am working with WPF and creating custom Messages control. I have message User Control and the message user controls showing in custom Messages control[ListBox].
XAML code:
<ListBox x:Name="uiMessages" ItemsSource="{Binding Path=Messages}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" IsItemsHost="True" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<wpfMessages:MessageDisplay>
</wpfMessages:MessageDisplay>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Current view:
Expected:
Now list box item is showing from top to bottom and after red message there empty space. I will be good whem the 2 green messages will bi in bottom and red one in top. Expected is add messages from botton to top no from top to botton like now.
any Ideas?
Thanks in advance Jamaxack!
It is a little unclear but I think you want to change the ItemsPanel Layout.
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel VerticalAlignment="Bottom"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
Created custom WrapPanel that reverses Elements
XAML code:
<ListBox x:Name="uiMessages" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<wpf:ReverseWrapPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
Code behind:
public class ReverseWrapPanel : WrapPanel
{
public new Orientation Orientation { get { return base.Orientation; } set { base.Orientation = value; ResetAll(); } }
/// <summary>
/// The opposite of the Orientation property.
/// </summary>
Orientation FlipDirection { get { return Orientation == Orientation.Horizontal ? Orientation.Vertical : Orientation.Horizontal; } }
public ReverseWrapPanel()
{
Initialized += ReverseWrapPanel_Initialized;
}
void ReverseWrapPanel_Initialized(object sender, System.EventArgs e)
{
this.Mirror(FlipDirection);
}
protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved)
{
base.OnVisualChildrenChanged(visualAdded, visualRemoved);
foreach (UIElement child in Children.OfType<UIElement>())
{
child.Mirror(FlipDirection);
}
}
void ResetAll()
{
this.Mirror(FlipDirection);
foreach (UIElement child in Children.OfType<UIElement>())
{
child.Mirror(FlipDirection);
}
}
}
Thanx Jamshed!

Is it possible to bind a Canvas's Children property in XAML?

I'm a little surprised that it is not possible to set up a binding for Canvas.Children through XAML. I've had to resort to a code-behind approach that looks something like this:
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
DesignerViewModel dvm = this.DataContext as DesignerViewModel;
dvm.Document.Items.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(Items_CollectionChanged);
foreach (UIElement element in dvm.Document.Items)
designerCanvas.Children.Add(element);
}
private void Items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
ObservableCollection<UIElement> collection = sender as ObservableCollection<UIElement>;
foreach (UIElement element in collection)
if (!designerCanvas.Children.Contains(element))
designerCanvas.Children.Add(element);
List<UIElement> removeList = new List<UIElement>();
foreach (UIElement element in designerCanvas.Children)
if (!collection.Contains(element))
removeList.Add(element);
foreach (UIElement element in removeList)
designerCanvas.Children.Remove(element);
}
I'd much rather just set up a binding in XAML like this:
<Canvas x:Name="designerCanvas"
Children="{Binding Document.Items}"
Width="{Binding Document.Width}"
Height="{Binding Document.Height}">
</Canvas>
Is there a way to accomplish this without resorting to a code-behind approach? I've done some googling on the subject, but haven't come up with much for this specific problem.
I don't like my current approach because it mucks up my nice Model-View-ViewModel by making the View aware of it's ViewModel.
<ItemsControl ItemsSource="{Binding Path=Circles}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="White" Width="500" Height="500" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Fill="{Binding Path=Color, Converter={StaticResource colorBrushConverter}}" Width="25" Height="25" />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Top" Value="{Binding Path=Y}" />
<Setter Property="Canvas.Left" Value="{Binding Path=X}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
Others have given extensible replies on how to do what you actually want to do already. I'll just explain why you couldn't bind Children directly.
The problem is very simple - data binding target cannot be a read-only property, and Panel.Children is read-only. There is no special handling for collections there. In contrast, ItemsControl.ItemsSource is a read/write property, even though it is of collection type - a rare occurence for a .NET class, but required so as to support the binding scenario.
ItemsControl is designed for creating dynamic collections of UI controls from other collections, even non-UI data collections.
You can template an ItemsControl to draw on a Canvas. The ideal way would involve setting the backing panel to a Canvas and then setting the Canvas.Left and Canvas.Top properties on the immediate children. I could not get this to work because ItemsControl wraps its children with containers and it is hard to set the Canvas properties on these containers.
Instead, I use a Grid as a bin for all of the items and draw them each on their own Canvas. There is some overhead with this approach.
<ItemsControl x:Name="Collection" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:MyPoint}">
<Canvas HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Ellipse Width="10" Height="10" Fill="Black" Canvas.Left="{Binding X}" Canvas.Top="{Binding Y}"/>
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Here's the code behind that I used to set up the source collection:
List<MyPoint> points = new List<MyPoint>();
points.Add(new MyPoint(2, 100));
points.Add(new MyPoint(50, 20));
points.Add(new MyPoint(200, 200));
points.Add(new MyPoint(300, 370));
Collection.ItemsSource = points;
MyPoint is a custom class that behaves just like the System version. I created it to demonstrate that you can use your own custom classes.
One final detail: You can bind the ItemsSource property to any collection you want. For example:
<ItemsControls ItemsSource="{Binding Document.Items}"><!--etc, etc...-->
For further details about ItemsControl and how it works, check out these documents: MSDN Library Reference; Data Templating; Dr WPF's series on ItemsControl.
internal static class CanvasAssistant
{
#region Dependency Properties
public static readonly DependencyProperty BoundChildrenProperty =
DependencyProperty.RegisterAttached("BoundChildren", typeof (object), typeof (CanvasAssistant),
new FrameworkPropertyMetadata(null, onBoundChildrenChanged));
#endregion
public static void SetBoundChildren(DependencyObject dependencyObject, string value)
{
dependencyObject.SetValue(BoundChildrenProperty, value);
}
private static void onBoundChildrenChanged(DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs e)
{
if (dependencyObject == null)
{
return;
}
var canvas = dependencyObject as Canvas;
if (canvas == null) return;
var objects = (ObservableCollection<UIElement>) e.NewValue;
if (objects == null)
{
canvas.Children.Clear();
return;
}
//TODO: Create Method for that.
objects.CollectionChanged += (sender, args) =>
{
if (args.Action == NotifyCollectionChangedAction.Add)
foreach (object item in args.NewItems)
{
canvas.Children.Add((UIElement) item);
}
if (args.Action == NotifyCollectionChangedAction.Remove)
foreach (object item in args.OldItems)
{
canvas.Children.Remove((UIElement) item);
}
};
foreach (UIElement item in objects)
{
canvas.Children.Add(item);
}
}
}
And using:
<Canvas x:Name="PART_SomeCanvas"
Controls:CanvasAssistant.BoundChildren="{TemplateBinding SomeItems}"/>
I don't believe its possible to use binding with the Children property. I actually tried to do that today and it errored on me like it did you.
The Canvas is a very rudimentary container. It really isn't designed for this kind of work. You should look into one of the many ItemsControls. You can bind your ViewModel's ObservableCollection of data models to their ItemsSource property and use DataTemplates to handle how each of the items is rendered in the control.
If you can't find an ItemsControl that renders your items in a satisfactory way, you might have to create a custom control that does what you need.

Categories

Resources