How to implement a WrapPanel with a header and footer - c#

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;
}
}
}

Related

Dynamic UI Layout in DataGridCell using ItemsControl and a Button

This screenshot is from the mockup of my ideal UI. Right now, this is a DataGridTemplateColumn, with header = "ATTENDEES". I am running into issues creating the layout of this DataGridColumn's cell.
I currently have an ItemsControl bound to a List of strings which are the attendees' emails. If there are too many attendees and the ItemsControls' bounds cannot fit in the cell, then a Button with Content = "See more" should appear at the bottom of the cell, under the last attendee email that can be rendered within in the cell's bounds.
Then once the Button ("See more") is clicked, the row should expand to an appropriate height for the attendees to all be visible, and the "See more" Button should disappear.
I could not wrap my head around a clean implementation with a TemplateSelector, ValueConverter, or DataTrigger in pure XAML since I need to compare the ItemsControls' height against the DataGridRow's height and then perform a modification of the cell's layout at runtime by hiding all the items in the ItemsControl that cannot fit within the cell and then showing at Button below it.
I concluded on attempting to do this in the code-behind by subscribing to the ItemControls' load event. I first attempted to use the Height, MaxHeight, DesiredSize.Height, RenderedSize.Height, and ActualSize.Height properties of the ItemsControl but those all were equal to the clipped height of the ItemsControl, not the intrinsic height of all its contents.
I am now measuring the total height of all its items' strings using the FormattedText class. Then I compare this summed height with the row's height and that's as far as I have progressed; I am unsure of how to next change the layout of the cell or if this is even the correct approach.
I feel like I am fighting against the design of the WPF framework by doing rudimentary calculations and crude layout changes to the view in the code-behind.
Any help on this would be greatly appreciated!
Here is my event handler for the ItemsControl.Load:
private void AttendeesItemsControl_Loaded(object sender, RoutedEventArgs e)
{
if (currentRowIndex == -1)
{
return;
}
List<ModelBase> eventsData = ModelManager.events.data;
var eventObj = (Event)eventsData[currentRowIndex];
var attendees = eventObj.attendees;
var totalItemsHeight = 0;
for(int i = 0; i < attendees.Count; i++)
{
totalItemsHeight += heightOfString(attendees[i]);
}
var itemsControl = (ItemsControl)sender;
var controlRenderHeight = itemsControl.RenderSize.Height;
// Check if the intrinsic height is greater than what can be drawn inside the cell
if (controlRenderHeight < totalItemsHeight)
{
var itemHeight = totalItemsHeight / attendees.Count;
var visibleItemsCount = controlRenderHeight / itemHeight;
// .... not sure how to proceed
}
}
And the helper function that measures the height of one of its items:
private int heightOfString(string candidate)
{
var fontFamily = new FontFamily("Lato");
var fontStyle = FontStyles.Normal;
var fontWeight = FontWeights.Normal;
var fontStretch = FontStretches.Normal;
var fontSize = 12;
var typeFace = new Typeface(fontFamily, fontStyle, fontWeight, fontStretch);
var formattedText = new FormattedText(candidate, CultureInfo.CurrentUICulture, FlowDirection.LeftToRight, typeFace, fontSize, Brushes.Black);
return (int)formattedText.Height;
}
Finally, this is the DataGridTemplateColumn's XAML, with the cell template definition:
<DataGridTemplateColumn Header="ATTENDEES" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding Path=attendees}" x:Name="AttendeesItemsControl" Loaded="AttendeesItemsControl_Loaded">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock FontFamily="Lato" FontSize="12" FontWeight="Normal" Text="{Binding}">
</TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
I had to do some real work but I got this set up. Hopefully you can follow it. Here is a screen shot of what it looks like. Obviously i didn't attempt to style it yet. Just getting the resizing. This way you let WPF handle the height of your control you leave it autosized. You just manage your list.
I created a control for the list called AttendeeListControl
<UserControl xmlns:stackoverflow="clr-namespace:stackoverflow" x:Class="stackoverflow.AttendeeListControl"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid Background="GhostWhite">
<Grid.RowDefinitions>
<RowDefinition Height="37"/>
<RowDefinition Height="*"/>
<RowDefinition Height="23"/>
</Grid.RowDefinitions>
<Label Content="Attendees" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top"/>
<ListBox Name="listBoxAttendees" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="1" />
<Button Content="SeeMore" Name="lblMore" HorizontalAlignment="Left" Margin="10,0,0,0" Grid.Row="2" VerticalAlignment="Top" Click="lblMore_Click"/>
</Grid>
</UserControl>
This is the code behind
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows.Controls;
namespace stackoverflow
{
/// <summary>
/// Interaction logic for AttendeeListControl.xaml
/// </summary>
///
public partial class AttendeeListControl : UserControl
{
public AttendeeListViewModel vm { get; set; }
public AttendeeListControl()
{
InitializeComponent();
var emails = new List<string>() { "email#gmail.com", "email#aol.com", "email.yahoo.com", "email#msn.com" };
var displayed = new ObservableCollection<string>() { emails[0], emails[1] };
vm = new AttendeeListViewModel()
{
EmailList = emails,
DisplayList = displayed,
Expanded = false
};
DataContext = vm;
listBoxAttendees.ItemsSource = vm.DisplayList;
}
private void lblMore_Click(object sender, System.Windows.RoutedEventArgs e)
{
if (vm.Expanded)
{
//remove all but last 2
do
{
vm.DisplayList.RemoveAt(vm.DisplayList.Count - 1);
} while (vm.DisplayList.Count > 2);
lblMore.Content = "Show More";
}
else
{
//don't want the first 2
for (int i = 2; i < vm.EmailList.Count; i++)
{
vm.DisplayList.Add(vm.EmailList[i]);
}
lblMore.Content = "Show Less";
}
vm.Expanded = !vm.Expanded;
}
}
}
and here is the model i used
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace stackoverflow
{
public class AttendeeListViewModel
{
public bool Expanded { get; set; }
public List<string> EmailList { get; set; }
public ObservableCollection<string> DisplayList { get; set; }
}
}
this was all just put on the mainwindow
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:stackoverflow" x:Class="stackoverflow.MainWindow"
Title="MainWindow" Height="350" Width="525">
<Grid>
<local:AttendeeListControl HorizontalAlignment="Left" Margin="55,53,0,0" VerticalAlignment="Top"/>
<local:AttendeeListControl HorizontalAlignment="Left" Margin="340,53,0,0" VerticalAlignment="Top"/>
</Grid>
</Window>

Creating a endless centered carousel control in wpf

I'm trying to create a endless, centered carousel in WPF like in this concept image. The current solution I've come up with is using a listbox, loading all images into a ObservableCollection and then modifying it to create the illusion of movement.
I have two issues with this solution. First I can't seem to center it. The listbox is aligned to the left with no way of getting it to overflow on both sides. Regardless of the size of my window it should always show one console in the middle, one on each sides and a half one to indicate that there's more to choose from.
The second issue is not as important, but I'm looking for a proper way of doing this that may allow a more fluent transition between selections later on.
This is my current code:
XAML:
<Window x:Class="SystemMenu.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<DockPanel>
<Button Content="left" Height="20" Click="Left_Click" DockPanel.Dock="Top" />
<Button Content="right" Height="20" Click="Right_Click" DockPanel.Dock="Top" />
<ListBox x:Name="LoopPanel" ItemsSource="{Binding Path=SampleData}" SelectedIndex="3" ScrollViewer.HorizontalScrollBarVisibility="Hidden" ScrollViewer.VerticalScrollBarVisibility="Hidden" ScrollViewer.CanContentScroll="False">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Image Source="{Binding}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
Code behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
}
ObservableCollection<string> sampleData = new ObservableCollection<string>();
public ObservableCollection<string> SampleData
{
get
{
if (sampleData.Count <= 0)
{
sampleData.Add(#"Nintendo 64.png");
sampleData.Add(#"Nintendo Famicom.png");
sampleData.Add(#"Super Nintendo Entertainment System.png");
sampleData.Add(#"Nintendo Entertainment System.png");
sampleData.Add(#"Sony PlayStation.png");
}
return sampleData;
}
}
private void Right_Click(object sender, RoutedEventArgs e)
{
var firstItem = SampleData.First();
SampleData.Remove(firstItem);
SampleData.Insert(SampleData.Count, firstItem);
}
private void Left_Click(object sender, RoutedEventArgs e)
{
var lastItem = SampleData.Last();
SampleData.Remove(lastItem);
SampleData.Insert(0, lastItem);
}
}
Edit:
I found the following extension solving the issue I had with centering the listbox. Calling LoopPanel.ScrollToCenterOfView(sampleData[2]); seems to do the trick of centering the images... Any idea now on how to animate the transition? :)
public static class ItemsControlExtensions
{
public static void ScrollToCenterOfView(this ItemsControl itemsControl, object item)
{
// Scroll immediately if possible
if (!itemsControl.TryScrollToCenterOfView(item))
{
// Otherwise wait until everything is loaded, then scroll
if (itemsControl is ListBox) ((ListBox)itemsControl).ScrollIntoView(item);
itemsControl.Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
{
itemsControl.TryScrollToCenterOfView(item);
}));
}
}
private static bool TryScrollToCenterOfView(this ItemsControl itemsControl, object item)
{
// Find the container
var container = itemsControl.ItemContainerGenerator.ContainerFromItem(item) as UIElement;
if (container == null) return false;
// Find the ScrollContentPresenter
ScrollContentPresenter presenter = null;
for (Visual vis = container; vis != null && vis != itemsControl; vis = VisualTreeHelper.GetParent(vis) as Visual)
if ((presenter = vis as ScrollContentPresenter) != null)
break;
if (presenter == null) return false;
// Find the IScrollInfo
var scrollInfo =
!presenter.CanContentScroll ? presenter :
presenter.Content as IScrollInfo ??
FirstVisualChild(presenter.Content as ItemsPresenter) as IScrollInfo ??
presenter;
// Compute the center point of the container relative to the scrollInfo
Size size = container.RenderSize;
Point center = container.TransformToAncestor((Visual)scrollInfo).Transform(new Point(size.Width / 2, size.Height / 2));
center.Y += scrollInfo.VerticalOffset;
center.X += scrollInfo.HorizontalOffset;
// Adjust for logical scrolling
if (scrollInfo is StackPanel || scrollInfo is VirtualizingStackPanel)
{
double logicalCenter = itemsControl.ItemContainerGenerator.IndexFromContainer(container) + 0.5;
Orientation orientation = scrollInfo is StackPanel ? ((StackPanel)scrollInfo).Orientation : ((VirtualizingStackPanel)scrollInfo).Orientation;
if (orientation == Orientation.Horizontal)
center.X = logicalCenter;
else
center.Y = logicalCenter;
}
// Scroll the center of the container to the center of the viewport
if (scrollInfo.CanVerticallyScroll) scrollInfo.SetVerticalOffset(CenteringOffset(center.Y, scrollInfo.ViewportHeight, scrollInfo.ExtentHeight));
if (scrollInfo.CanHorizontallyScroll) scrollInfo.SetHorizontalOffset(CenteringOffset(center.X, scrollInfo.ViewportWidth, scrollInfo.ExtentWidth));
return true;
}
private static double CenteringOffset(double center, double viewport, double extent)
{
return Math.Min(extent - viewport, Math.Max(0, center - viewport / 2));
}
private static DependencyObject FirstVisualChild(Visual visual)
{
if (visual == null) return null;
if (VisualTreeHelper.GetChildrenCount(visual) == 0) return null;
return VisualTreeHelper.GetChild(visual, 0);
}
}
I don't think I would do it how you're doing it. I.e. adding and removing items in a ListBox. Doesn't give you enough control on the positioning and you won't be able to do smooth animations of it rotating which with that kind of UI, I think that would be kind of expected :).
I'd probably have a Canvas instead with ClipToBounds=true. Then just calculate the positions, you aren't doing a rounded carousel, so positions are trivial and there is no zooming.
Lets say your images are all 100 x 100. So item0 will be # -50,0, item1 # 50,0 (well, technically probably 75,0 or whatever because you'd want some spacing between them, but you get the idea), etc. Because you are calculating the positions and have them absolute against the Canvas, the ClipToBound=true will clip the two on either end and you'll be able to animate the rotation.

WPF databinding a user control

Ive been looking for hours on a solution to this.
I have a tab Adapter class that im using to fill a tab control
public partial class TabAdapter : UserControl
{
public static readonly DependencyProperty fileNameProperty =
DependencyProperty.Register(
"fileName",
typeof(string),
typeof(TabAdapter),
new FrameworkPropertyMetadata(
string.Empty,
FrameworkPropertyMetadataOptions.AffectsRender,
new PropertyChangedCallback(OnFileNamePropertyChanged),
new CoerceValueCallback(coerceFileName)
),
new ValidateValueCallback(fileNameValidationCallback)
);
public TabAdapter()
{
InitializeComponent();
//initializeInterior();
CreateSaveCommand();
TabAdapterContent.DataContext = this;
Console.WriteLine("constructor hit.");
}
public string fileName
{
get { return (string)GetValue(fileNameProperty); }
set { SetValue(fileNameProperty, value); }
}
private ColumnMapper _columnMap;
private TableMapper _tableMap;
private TabType tabType;
private enum TabType { TABLE_MAPPER, COLUMN_MAPPER, ERROR_MSG }
private static object coerceFileName(DependencyObject d, object value)
{
return fileName;
}
private static bool fileNameValidationCallback(object Value)
{
string fn = (string)Value;
if (fn.Equals(string.Empty))
{
return true;
}
FileInfo fi = new FileInfo(fn);
return ((fi.Exists && fi.Extension.Equals(".csv")));
}
private static void OnFileNamePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs args)
{
TabAdapter source = d as TabAdapter;
Console.WriteLine("got to property changer: " + (string)args.NewValue + " :new / old: " + (string)args.OldValue);
source.initializeInterior();
}
private void initializeInterior()
{
Console.WriteLine("initializing Interior filename: " + fileName);
if (Regex.IsMatch(fileName, #".*_SourceTableMapping.csv$"))
{
tabType = TabType.TABLE_MAPPER;
_tableMap = new TableMapper(fileName);
Grid.SetRow(_tableMap, 0);
Grid.SetColumn(_tableMap, 0);
//clear out the content.
this.TabAdapterContent.Children.Clear();
//add new content
this.TabAdapterContent.Children.Add(_tableMap);
}
else if (fileName.EndsWith(".csv"))
{
tabType = TabType.TABLE_MAPPER;
_columnMap = new ColumnMapper(fileName);
Grid.SetRow(_columnMap, 0);
Grid.SetColumn(_columnMap, 0);
//clear out the content.
this.TabAdapterContent.Children.Clear();
//add new content
this.TabAdapterContent.Children.Add(_columnMap);
}
else
{
tabType = TabType.ERROR_MSG;
TextBlock tb = new TextBlock();
tb.Text = "The File: " + fileName + " is not a valid mapping file.";
Grid.SetRow(tb, 0);
Grid.SetColumn(tb, 0);
//clear out the content.
this.TabAdapterContent.Children.Clear();
//add new content
this.TabAdapterContent.Children.Add(tb);
}
}
}
The point of this is to decide what type of file is being added and load up the correct user control inside of it to display that file.
my main window xaml for the tab control is
<TabControl x:Name="tabControl" Grid.Column="1" Grid.Row="0" ItemsSource="{Binding openFileNames, Mode=OneWay}">
<TabControl.LayoutTransform>
<!-- Allows to zoom the control's content using the slider -->
<ScaleTransform CenterX="0"
CenterY="0" />
<!-- part of scale transform ScaleX="{Binding ElementName=uiScaleSlider,Path=Value}"
ScaleY="{Binding ElementName=uiScaleSlider,Path=Value}" />-->
</TabControl.LayoutTransform>
<!-- Header -->
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Header}" />
</DataTemplate>
</TabControl.ItemTemplate>
<!-- Content -->
<TabControl.ContentTemplate>
<DataTemplate>
<local:TabAdapter fileName="{Binding fileName}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
the header works, and if i changed
<local:TabAdapter fileName="{Binding fileName}" />
into
<TextBlock Text="{Binding fileName}" />
Then it all binds correctly, I have a feeling it has something to do with the data context on my tab adapter. but not exactly sure what it needs to be set to.
my xaml for the tab adapter is
<UserControl x:Class="ImportMappingGui.TabAdapter"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid x:Name="TabAdapterContent">
<Grid.RowDefinitions>
<RowDefinition Height = "*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
</Grid>
it will all compile and run, but only the constructor of the user control gets hit, and at that only once no matter how many tabs I create.
This is my first WPF application so my apologies if it is something stupid im missing.
(or if my methodology of setting up a adapter of sorts is not the best way of solving this issue).
Do not set the DataContext of a UserControl from within the UserControl itself. It defeats the whole point of having "lookless" controls that can be used to display whatever you pass it.
Your fileName binding is failing because the DataContext = this, and this is the TabAdapter control itself, and TabAdapter.fileName is not actually set to anything. (Remember, binding to tell a property to retrieve it's value somewhere else is different from setting the value directly)
As for the constructor not running more than once, that is by design. Since you told the TabControl to use the TabAdapter control as a template, it will create one copy of the TabAdapter, and simply replace the .DataContext behind the control whenever you switch tabs. This increases performance as it doesn't have to keep initializing and tracking a separate control for each tab, and reduces the memory used.

WPF "Hidden" behind a Panel

I am encountering an issue in my project and i don't have much more time to do research.
The goal of this project is to allow attending to tests without being physically present.
You receive a file containing your tests, you attend to them (time limited) and send back the file containing your answers.
I got a TextBox in a StackPanel, itself contained in another StackPanel.
All the controls are created programatically.
The controls are added correctly but the TextBox don't react to mouse input.... (in fact only when the textbox is the ast item and even ther only the little last pixel)
UserControl XAML file :
<UserControl x:Class="DataLibrary.View.Questions.ListQuestionInterface"
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:localization ="clr-namespace:DataLibrary.Resources"
xmlns:convert ="clr-namespace:DataLibrary.View.Converters"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
Loaded="ListQuestionInterface_OnLoaded">
<UserControl.Resources>
<localization:LocalizedStrings x:Key="LocalizedStrings"/>
<convert:getVisible x:Key="getVisible"/>
<convert:getText x:Key="getText"/>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="10*"/>
<RowDefinition Height="40"/>
</Grid.RowDefinitions>
<StackPanel VerticalAlignment="Stretch" Orientation="Horizontal" x:Name="body" Grid.Row="0" FocusManager.IsFocusScope="True"/>
<Label Grid.Row="0" Margin="0,10,0,0" x:Name="explanations"/>
<Button Content="{Binding Path=type, Converter={StaticResource getText}}"
HorizontalAlignment="Right"
Margin="0,0,10,10"
VerticalAlignment="Bottom"
Grid.Row="1"
Width="120"
Height="20"
Click="DisplayAnswerButton_Click"
Visibility="{Binding Path=type, Converter={StaticResource getVisible}}"/>
</Grid>
</UserControl>
Code Behind:
public partial class ListQuestionInterface : UserControl
{
private UIElement _firstElement;
ListQuestion q;
private bool isTest;
public questionType type
{
get
{
return q.Type;
}
set
{
Console.WriteLine("Attempted to write questionType");
}
}
public ListQuestionInterface(ListQuestion question, bool isTest = true)
{
InitializeComponent();
this.explanations.Content = question.Explanation;
this.DataContext = this;
this.q = question;
this.isTest = isTest;
refreshStackPanel();
}
private void refreshStackPanel()
{
bool first = true;
this.body.Children.Clear();
var enumerators = new Hashtable();
foreach (Question subQuestion in this.q.SubQuestions)
{
enumerators.Add(subQuestion, subQuestion.interfaceEnumerator(isTest).GetEnumerator());
((IEnumerator)enumerators[subQuestion]).MoveNext();
}
//If the Alignemnt property has a value we'll want each pair of control to be aligned wit heach other
//if not, we just want them stacked to the left
if (q.Alignment.HasValue)
{
int maxCount = this.q.SubQuestions.Max(x => x.interfaceEnumerator(isTest).Count());
for (int i = 0; i < maxCount; i++)
{
var stack = new StackPanel
{
VerticalAlignment = VerticalAlignment.Stretch,
HorizontalAlignment = HorizontalAlignment.Center
};
foreach (Question subQuestion in this.q.SubQuestions)
{
try
{
var enumerator = (IEnumerator)enumerators[subQuestion];
var control = enumerator.Current as Control;
((Panel)VisualTreeHelper.GetParent(control)).Children.Remove(control);
control.HorizontalAlignment = q.Alignment.Value;
Canvas canvas = null;
if (control.GetType() == typeof(Button) || control.GetType() == typeof(MaskedTextBox))
{
canvas = new Canvas();
if (control.GetType() == typeof(MaskedTextBox))
{
var thick = control.Margin;
thick.Left -= 5;
control.Margin = thick;
}
if (first)
{
this._firstElement = control;
first = false;
}
control.Focusable = true;
canvas.Children.Add(control);
}
if (canvas == null)
{
stack.Children.Add(control);
}
else
{
stack.Children.Add(canvas);
}
enumerator.MoveNext();
}
catch
{
var blank = new Label
{
Content = "BLANK",
Visibility = Visibility.Hidden
};
stack.Children.Add(blank);
Console.WriteLine("No more items to display");
}
}
this.body.Children.Add(stack);
}
}
else
{
var stack = new StackPanel
{
VerticalAlignment = VerticalAlignment.Stretch,
HorizontalAlignment = HorizontalAlignment.Center,
};
foreach (var subQuestion in q.SubQuestions)
{
var subStack = new StackPanel
{
VerticalAlignment = VerticalAlignment.Stretch,
HorizontalAlignment = HorizontalAlignment.Left,
Orientation = Orientation.Horizontal
};
var enumerator = subQuestion.interfaceEnumerator(isTest).GetEnumerator();
while (enumerator.MoveNext())
{
var control = enumerator.Current as Control;
control.HorizontalAlignment = HorizontalAlignment.Left;
if (control.GetType() == typeof(Button) || control.GetType() == typeof(MaskedTextBox))
{
if (first)
{
this._firstElement = control;
first = false;
}
control.Focusable = true;
}
((Panel)VisualTreeHelper.GetParent(control)).Children.Remove(control);
subStack.Children.Add(control);
}
stack.Children.Add(subStack);
}
this.body.Children.Add(stack);
}
}
private void DisplayAnswerButton_Click(object sender, RoutedEventArgs e)
{
foreach (Question question in q.SubQuestions)
{
question.DisplayAnswers();
}
refreshStackPanel();
}
private void ListQuestionInterface_OnLoaded(object sender, RoutedEventArgs e)
{
this._firstElement.Focus();
}
}
}
I don't think the button have somethng to do with the problem so i'll leave the converters code away.
I've checked some things already :
IsHitTestVisible is never set to false
Defining one of the stackpanels as the FocusScope don't change anything
If i place all my controls into canvas and the canvas into the stackpanel i can freely click on my controls but their placement is completely broken.
The Actual Width/height of the control is sufficient to interact with them(60x20)
The problem seems to have appeared just after using the following trick to set a first focused element on other UserControls (which are not in the current VisualTree anymore)
I really do need help since i can't seem to find someone with a similar problem.
And here are two screenshots to illustrate the problem :
The black arrows shows where my i clicked before taking the screenshot (btw if you know of any software that can do a screenshot WITH the mouse i'm taking it :) )
Ok, my fault here -_-'
I was so tired that i didnt't see that my stackpanel was in fact really BEHIND a Label
In my code i only had 2 row definitions, in the first i put the stackpanel AND a Label (who took the entire space).
And because it was declared later, the label was above the stackpanel thus preventing any mouse interaction with it's content.
Here is the corrected XAML :
<UserControl x:Class="DataLibrary.View.Questions.ListQuestionInterface"
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:localization ="clr-namespace:DataLibrary.Resources"
xmlns:convert ="clr-namespace:DataLibrary.View.Converters"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
Loaded="ListQuestionInterface_OnLoaded">
<UserControl.Resources>
<localization:LocalizedStrings x:Key="LocalizedStrings"/>
<convert:getVisible x:Key="getVisible"/>
<convert:getText x:Key="getText"/>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="60*"/>
<RowDefinition Height="20*"/>
<RowDefinition Height="10*"/>
</Grid.RowDefinitions>
<StackPanel VerticalAlignment="Stretch" Orientation="Horizontal" x:Name="body" Grid.Row="0" FocusManager.IsFocusScope="True"/>
<Label Grid.Row="1" Margin="0,10,0,0" x:Name="explanations"/>
<Button Content="{Binding Path=type, Converter={StaticResource getText}}"
HorizontalAlignment="Right"
Margin="0,0,10,10"
VerticalAlignment="Bottom"
Grid.Row="2"
Width="120"
Height="20"
Click="DisplayAnswerButton_Click"
Visibility="{Binding Path=type, Converter={StaticResource getVisible}}"/>
</Grid>
</UserControl>
So in fact, the control was really hidden behind another one -_-'
I now know that this isn't the WPF way of doing things, but i do not know yet how to DataBind to a Template properly
I'm still taking any advices on a good tutorial/starting point for binding to a DataTemplate in XAML, tho only DataBindings i could find were for binding single values to control properties

Choppy movement of large collection of items on a canvas

This question is directly related to a question I recently posted, but I feel that the direction has changed enough to warrant a new one. I am trying to figure out the best way to move a large collection of images on a canvas in real-time. My XAML currently looks like this:
<UserControl.Resources>
<DataTemplate DataType="{x:Type local:Entity}">
<Canvas>
<Image Canvas.Left="{Binding Location.X}"
Canvas.Top="{Binding Location.Y}"
Width="{Binding Width}"
Height="{Binding Height}"
Source="{Binding Image}" />
</Canvas>
</DataTemplate>
</UserControl.Resources>
<Canvas x:Name="content"
Width="2000"
Height="2000"
Background="LightGreen">
<ItemsControl Canvas.ZIndex="2" ItemsSource="{Binding Entities}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
The Entity class:
[Magic]
public class Entity : ObservableObject
{
public Entity()
{
Height = 16;
Width = 16;
Location = new Vector(Global.rand.Next(800), Global.rand.Next(800));
Image = Global.LoadBitmap("Resources/Thing1.png");
}
public int Height { get; set; }
public int Width { get; set; }
public Vector Location { get; set; }
public WriteableBitmap Image { get; set; }
}
To move the object:
private Action<Entity> action = (Entity entity) =>
{
entity.Location = new Vector(entity.Location.X + 1, entity.Location.Y);
};
void Timer_Tick(object sender, EventArgs e)
{
Task.Factory.StartNew(() =>
{
foreach (var entity in Locator.Container.Entities)
{
action(entity);
}
});
}
If I have fewer than about 400 entries in the Entities collection, movement is smooth, but I'd like to be able to increase that number by quite a bit. If I go above 400, movement becomes increasingly choppy. At first I thought it was an issue with the movement logic (which at this point isn't really much of anything), but I have found that that's not the problem. I added another collection with 10,000 entries and added that collection to the same timer loop as the first but did not include it in the XAML, and the UI didn't react any differently. What I find odd, however, is that if I add 400 entries to the collection and then 400 more with Image set to null, movement becomes choppy even though half of the items aren't drawn.
So, what can I do, if anything, to be able to draw and smoothly move more images on a canvas? Is this a situation where I may want to shy away from WPF & XAML? If you need more code, I will gladly post it.
Update: Per Clemens' suggestion, my Entity DataTemplate now looks like this:
<DataTemplate DataType="{x:Type local:Entity}">
<Image Width="{Binding Width}"
Height="{Binding Height}"
Source="{Binding Image}">
<Image.RenderTransform>
<TranslateTransform X="{Binding Location.X}" Y="{Binding Location.Y}" />
</Image.RenderTransform>
</Image>
</DataTemplate>
There may be a boost in performance by using this, but if there is it is very subtle. Also, I have noticed that if I use a DispatcherTimer for the loop and set it up as:
private DispatcherTimer dTimer = new DispatcherTimer();
public Loop()
{
dTimer.Interval = TimeSpan.FromMilliseconds(30);
dTimer.Tick += Timer_Tick;
dTimer.Start();
}
void Timer_Tick(object sender, EventArgs e)
{
foreach (var entity in Locator.Container.Entities)
{
action(entity);
}
}
... The movement is smooth even with several thousand items, but very slow, regardless of the interval. If a DispatcherTimer is used and Timer_Tick looks like this:
void Timer_Tick(object sender, EventArgs e)
{
Task.Factory.StartNew(() =>
{
foreach (var entity in Locator.Container.Entities)
{
action(entity);
}
});
}
... the movement is very choppy. What I find odd is that a Stopwatch shows that the Task.Factory takes between 1000 and 1400 ticks to iterate over the collection if there are 5,000 entries. The standard foreach loop takes over 3,000 ticks. Why would Task.Factory perform so poorly when it is twice as fast? Is there a different way to iterate through the collection and/or a different timing method that might allow for smooth movement without any major slowdowns?
Update: If anybody can help me improve the performance of real-time movement of objects on a canvas or can suggest another way in WPF to achieve similar results, 100 bounty awaits.
Having so many controls move on the screen this frequently will never yield smooth results. You need to a completely different approach - rendering on your own. I'm not sure this would suit you, as now you will not be able to use control features per each item (e.g. to receive events, have tooltips or use data templates.) But with such a large amount of items, other approaches are impractical.
Here's a (very) rudimentary implementation of what that might look like:
Update: I've modified the renderer class to use the CompositionTarget.Rendering event instead of a DispatcherTimer. This event fires every time WPF renders a frame (normally around 60 fps). While this would provide smoother results, it is also more CPU intensive, so be sure to turn off the animation when it's no longer needed.
public class ItemsRenderer : FrameworkElement
{
private bool _isLoaded;
public ItemsRenderer()
{
Loaded += OnLoaded;
Unloaded += OnUnloaded;
}
private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
_isLoaded = true;
if (IsAnimating)
{
Start();
}
}
private void OnUnloaded(object sender, RoutedEventArgs routedEventArgs)
{
_isLoaded = false;
Stop();
}
public bool IsAnimating
{
get { return (bool)GetValue(IsAnimatingProperty); }
set { SetValue(IsAnimatingProperty, value); }
}
public static readonly DependencyProperty IsAnimatingProperty =
DependencyProperty.Register("IsAnimating", typeof(bool), typeof(ItemsRenderer), new FrameworkPropertyMetadata(false, (d, e) => ((ItemsRenderer)d).OnIsAnimatingChanged((bool)e.NewValue)));
private void OnIsAnimatingChanged(bool isAnimating)
{
if (_isLoaded)
{
Stop();
if (isAnimating)
{
Start();
}
}
}
private void Start()
{
CompositionTarget.Rendering += CompositionTargetOnRendering;
}
private void Stop()
{
CompositionTarget.Rendering -= CompositionTargetOnRendering;
}
private void CompositionTargetOnRendering(object sender, EventArgs eventArgs)
{
InvalidateVisual();
}
public static readonly DependencyProperty ImageSourceProperty =
DependencyProperty.Register("ImageSource", typeof (ImageSource), typeof (ItemsRenderer), new FrameworkPropertyMetadata());
public ImageSource ImageSource
{
get { return (ImageSource) GetValue(ImageSourceProperty); }
set { SetValue(ImageSourceProperty, value); }
}
public static readonly DependencyProperty ImageSizeProperty =
DependencyProperty.Register("ImageSize", typeof(Size), typeof(ItemsRenderer), new FrameworkPropertyMetadata(Size.Empty));
public Size ImageSize
{
get { return (Size) GetValue(ImageSizeProperty); }
set { SetValue(ImageSizeProperty, value); }
}
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof (IEnumerable), typeof (ItemsRenderer), new FrameworkPropertyMetadata());
public IEnumerable ItemsSource
{
get { return (IEnumerable) GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
protected override void OnRender(DrawingContext dc)
{
ImageSource imageSource = ImageSource;
IEnumerable itemsSource = ItemsSource;
if (itemsSource == null || imageSource == null) return;
Size size = ImageSize.IsEmpty ? new Size(imageSource.Width, imageSource.Height) : ImageSize;
foreach (var item in itemsSource)
{
dc.DrawImage(imageSource, new Rect(GetPoint(item), size));
}
}
private Point GetPoint(object item)
{
var args = new ItemPointEventArgs(item);
OnPointRequested(args);
return args.Point;
}
public event EventHandler<ItemPointEventArgs> PointRequested;
protected virtual void OnPointRequested(ItemPointEventArgs e)
{
EventHandler<ItemPointEventArgs> handler = PointRequested;
if (handler != null) handler(this, e);
}
}
public class ItemPointEventArgs : EventArgs
{
public ItemPointEventArgs(object item)
{
Item = item;
}
public object Item { get; private set; }
public Point Point { get; set; }
}
Usage:
<my:ItemsRenderer x:Name="Renderer"
ImageSize="8 8"
ImageSource="32.png"
PointRequested="OnPointRequested" />
Code Behind:
Renderer.ItemsSource = Enumerable.Range(0, 2000)
.Select(t => new Item { Location = new Point(_rng.Next(800), _rng.Next(800)) }).ToArray();
private void OnPointRequested(object sender, ItemPointEventArgs e)
{
var item = (Item) e.Item;
item.Location = e.Point = new Point(item.Location.X + 1, item.Location.Y);
}
You can use the OnPointRequested approach to get any data from the item (such as the image itself.) Also, don't forget to freeze your images, and pre-resize them.
A side note, regarding threading in the previous solutions. When you use a Task, you're actually posting the property update to another thread. Since you've bound the image to that property, and WPF elements can only be updated from the thread on which they were created, WPF automatically posts each update to the Dispatcher queue to be executed on that thread. That's why the loop ends faster, and you're not timing the actual work of updating the UI. It's only adding more work.
In a first optimization approach you may reduce the number of Canvases to just one by removing the Canvas from the DataTemplate and setting Canvas.Left and Canvas.Top in an ItemContainerStyle:
<DataTemplate DataType="{x:Type local:Entity}">
<Image Width="{Binding Width}" Height="{Binding Height}" Source="{Binding Image}"/>
</DataTemplate>
<ItemsControl ItemsSource="{Binding Entities}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding Location.X}"/>
<Setter Property="Canvas.Top" Value="{Binding Location.Y}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
Then you may replace setting Canvas.Left and Canvas.Top by applying a TranslateTransform:
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="RenderTransform">
<Setter.Value>
<TranslateTransform X="{Binding Location.X}" Y="{Binding Location.Y}"/>
</Setter.Value>
</Setter>
</Style>
</ItemsControl.ItemContainerStyle>
Now this could similarly be applied to the Image control in the DataTemplate instead of the item container. So you may remove the ItemContainerStyle and write the DataTemplate like this:
<DataTemplate DataType="{x:Type local:Entity}">
<Image Width="{Binding Width}" Height="{Binding Height}" Source="{Binding Image}">
<Image.RenderTransform>
<TranslateTransform X="{Binding Location.X}" Y="{Binding Location.Y}"/>
</Image.RenderTransform>
</Image>
</DataTemplate>
Try using TranslateTransform instead of Canvas.Left and Canvas.Top. The RenderTransform and TranslateTransform are efficient in scaling/moving existing drawing objects.
That's an issue I had to solve when developping a very simple Library called Mongoose.
I tried it with a 1000 images and its totally smooth (I don't have code that automatically moves images, I move them manually by drag and dropping on the Surface, but you should have the same result with code).
I wrote a quick sample you can run by using the library (you just need an attached view model with a collection of anything called PadContents) :
MainWindow.xaml
<Window x:Class="Mongoose.Sample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:col="clr-namespace:System.Collections;assembly=mscorlib"
xmlns:mwc="clr-namespace:Mongoose.Windows.Controls;assembly=Mongoose.Windows"
Icon="Resources/MongooseLogo.png"
Title="Mongoose Sample Application" Height="1000" Width="1200">
<mwc:Surface x:Name="surface" ItemsSource="{Binding PadContents}">
<mwc:Surface.ItemContainerStyle>
<Style TargetType="mwc:Pad">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Image Source="Resources/MongooseLogo.png" Width="30" Height="30" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</mwc:Surface.ItemContainerStyle>
</mwc:Surface>
</Window>
MainWindow.xaml.cs
using System.Collections.ObjectModel;
using System.Windows;
namespace Mongoose.Sample
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
public ObservableCollection<object> PadContents
{
get
{
if (padContents == null)
{
padContents = new ObservableCollection<object>();
for (int i = 0; i < 500; i++)
{
padContents.Add("Pad #" + i);
}
}
return padContents;
}
}
private ObservableCollection<object> padContents;
}
}
And here is what it looks like for 1000 images :
The full code is available on Codeplex so even if you don't want to reuse the library, you can still check the code to see how achieved it.
I rely on a few tricks, but mostly the use of RenderTransform and CacheMode.
On my computer it's ok for up to 3000 images. If you want to do more, you'll probably have to think of other ways to achieve it though (maybe with some kind of virtualization)
Good luck !
EDIT:
By adding this code in the Surface.OnLoaded method :
var messageTimer = new DispatcherTimer();
messageTimer.Tick += new EventHandler(surface.messageTimer_Tick);
messageTimer.Interval = new TimeSpan(0, 0, 0, 0, 10);
messageTimer.Start();
And this method in the Surface class :
void messageTimer_Tick(object sender, EventArgs e)
{
var pads = Canvas.Children.OfType<Pad>();
if (pads != null && Layout != null)
{
foreach (var pad in pads)
{
pad.Position = new Point(pad.Position.X + random.Next(-1, 1), pad.Position.Y + random.Next(-1, 1));
}
}
}
You can see that it's totally ok to move each object separately.
Here is a samll example with 2000 objects
The issue here is the rendering/creation of so many controls.
The first question is whether you need to show all the images on the canvas. If so, I'm sorry but I can't help (if you need to draw all items then there's no way around it).
But if not all items are visible on the screen at one time - then you have hope in the shape of Virtualization. You'd need to write your own VirtualizingCanvas that inherits VirtualizingPanel and creates only the items that are visible. This will also allow you to recycle the containers which in turn will remove a lot of the load.
There's an example of a virtualizing canvas here.
Then you'd need to set the new canvas as your items panel, and set up the items to have the necessary information for the canvas to work properly.
A few thoughts that come to mind:
Freeze your bitmaps.
Hard set the size of your bitmaps when you read them to be identical to the size you're displaying them in, and set the BitmapScalingMode to LowQuality.
Track your progress while updating your entities and cut out early if you can't and grab them next frame. This will require tracking their last frame too.
// private int _lastEntity = -1;
// private long _tick = 0;
// private Stopwatch _sw = Stopwatch.StartNew();
// private const long TimeSlice = 30;
// optional: this._sw.Restart();
var end = this._sw.ElapsedMilliseconds + TimeSlice - 1;
this._tick++;
var ee = this._lastEntity++;
do {
if (ee >= this._entities.Count) ee = 0;
// entities would then track the last time
// they were "run" and recalculate their movement
// from 'tick'
action(this._entities[ee], this._tick);
if (this._sw.ElapsedMilliseconds > end) break;
} while (ee++ != this._lastEntity);
this._lastEntity = ee;

Categories

Resources