I have been trying to implement a feature into my application where when a user hovers over a Grid item housed within a GridView for a couple seconds it will display a medium sized popup Grid with more details. I have tried a few methods I found online, but I have been running into errors that I am not understanding the root cause of, and I have the feeling there is a much simpler approach to add this feature than what I have found online so far.
First, I tried a solution I adapted by using a derivative class of Grid. I ran into some issues with that that (detailed a bit more in my last question) with the main issue being that my Timer no longer would trigger and items using my "HoverGrid" data template within the GridView template would no longer show Image as a child item of the HoverGrid. So, I abandoned that approach.
Then I tried to implement the Timer directly in my Page's code-behind, which seemed to work (partially) as it is properly triggering PointerEnter, PointerExit, and TimerElapsed events, however, when trying to manipulate anything UI related in the TimerElapsed event I would get:
System.Exception: 'The application called an interface that was marshalled for a different thread. (Exception from HRESULT: 0x8001010E (RPC_E_WRONG_THREAD))'
Here is the XAML:
<Page
x:Class="myproject.Pages.MyPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Height="950"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
mc:Ignorable="d">
<Grid x:Name="ContentGrid">
<ListView
x:Name="AreaPanels"
Margin="0,10,0,0"
HorizontalContentAlignment="Stretch"
SelectionMode="None">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel AreStickyGroupHeadersEnabled="True" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<controls:AdaptiveGridView
CanDragItems="True"
DesiredWidth="140"
ItemHeight="140">
<controls:AdaptiveGridView.ItemTemplate>
<DataTemplate x:Name="IconTextTemplate" x:DataType="Image">
<Grid
PointerEntered="ImageHoverStart"
PointerExited="ImageHoverEnd">
<Image
Opacity="1"
Source="/Assets/Placeholders/sample_image.jpg"
Stretch="UniformToFill" />
</Grid>
</DataTemplate>
</controls:AdaptiveGridView.ItemTemplate>
<Grid />
<Grid />
<Grid />
<Grid />
</controls:AdaptiveGridView>
</ListView>
</Grid>
</Page>
Code-behind (C#):
using System.Diagnostics;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
namespace myproject.Pages
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MyPage : Page
{
//Variables for Grid functions
public object hoveredGridItem = null;
public PointerRoutedEventArgs hoveredGridItemArgs = null;
public System.Timers.Timer hoverTimer = new System.Timers.Timer();
public event System.Timers.ElapsedEventHandler TimerElapsed
{ add { hoverTimer.Elapsed += value; } remove { hoverTimer.Elapsed -= value; } }
public MyPage()
{
this.InitializeComponent();
hoverTimer.Enabled = true;
hoverTimer.Interval = 2000;
TimerElapsed += OnTimerElapsed;
}
private void ImageHoverStart(object sender, PointerRoutedEventArgs e)
{
Debug.WriteLine("Fired: ImageHoverStart");
hoverTimer.Start();
hoveredGridItem = sender;
hoveredGridItemArgs = e;
}
private void ImageHoverEnd(object sender, PointerRoutedEventArgs e)
{
Debug.WriteLine("Fired: ImageHoverEnd");
hoverTimer.Stop();
hoveredGridItem = null;
hoveredGridItemArgs = null;
}
private void OnTimerElapsed(object source, System.Timers.ElapsedEventArgs e)
{
Debug.WriteLine("Timer elapsed!");
hoverTimer.Stop();
if (hoveredGridItem.GetType().ToString() == "Windows.UI.Xaml.Controls.Grid")
{
Debug.WriteLine(hoveredGridItem.ToString());
Debug.WriteLine(hoveredGridItemArgs.ToString());
//Get the hovered image and associated arguments that were stored
Grid itm = (Grid)hoveredGridItem;
PointerRoutedEventArgs f = hoveredGridItemArgs;
//Get image position and bounds
//GeneralTransform transform = itm.TransformToVisual(Window.Current.Content);
//Point coordinatePointToWindow = transform.TransformPoint(new Point(0, 0));
//Rect winBounds = Window.Current.Bounds;
//Testing other UI items
itm.Visibility = Visibility.Visible;
// other UI stuff ...
}
}
}
}
I tried to make references to some UI elements such as Window.Content.Current and other elements (as a workaround) but was still getting the same System.Exception. I understand this has something to do with TimerElapsed being on a different thread than the UI thread and looked around for how to fix this but was not able to fix it.
My two issues are that I was not able to fix the thread marshalling issue (some issues with running things async) but more importantly that the solution seemed a bit convoluted, maybe more difficult than it needs to be.
At first to fix the threading issue you have to use the Dispatcher of the UIElement:
await itm.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
itm.Visibility = Visibility.Visible;
});
And second, have you thought about using a tooltip for this? It should be very easy to implement:
<Grid Background="Transparent">
<Image Opacity="1" Source="/Assets/StoreLogo.png" Stretch="UniformToFill" />
<!--Tooltip implementation:-->
<ToolTipService.ToolTip>
<Image Source="/Assets/StoreLogo.png"/>
</ToolTipService.ToolTip>
</Grid>
Trying to call the mainwindow update progressbar method from another class. The progressbar is not getting updated.
XAML Code
<ProgressBar x:Name="ProgressSplit" Margin="0,0,0,0" HorizontalAlignment="Center" VerticalAlignment="Center"
Grid.Row="11" Grid.Column="2" Minimum="0" Maximum="100" Foreground="#ffe600"
Width="700" Height="30" />
<TextBlock x:Name="txtProgressText" Text="{Binding ElementName=ProgressSplit, Path=Value, StringFormat={}{0:0}%}" Grid.Row="11" Grid.Column="2"
HorizontalAlignment="Center" VerticalAlignment="Center" />
MainWindow.xaml code
private delegate void UpdateProgressBarDelegate(DependencyProperty dp, Object value);
public void UpdateProgressBar(double value)
{
var updatePbDelegate = new UpdateProgressBarDelegate(ProgressSplit.SetValue);
Dispatcher.CurrentDispatcher.Invoke(updatePbDelegate, DispatcherPriority.Background, new object[] { RangeBase.ValueProperty, value });
}
Trying to call the public method from another class to update the progress bar
var mainWindow = (MainWindow)Application.Current.MainWindow;
mainWindow.UpdateProgressBar(20);
I'm not sure what the specific problem with your code is, but that is not the recommended way to handle progress. One problem is that you rely on a public property of the main window, so you have a tight coupling between your UI and your worker method.
Typically you should do something like
public async void OnButtonpress(){
var progress = new Progress<int>(Update);
void Update(int value) => ProgressSplit.Value = value;
try{
await Task.Run(() => WorkerMethod(progress));
}
catch{
...
}
}
public void WorkerMethod(IProgress<int> progress){
// on background thread
for(int i = 0; i< 100; i++){
// do work
progress.Report(i);
}
}
The progress class will take care of updating the progress bar on the UI thread. This helps decouple the UI from the worker method. This pattern is also amenable to adding things like cancellation etc. Just remember to take care to set the maximal value of the progress bar. Personally I prefer to always use a double to report progress, where 1.0 represent a completed task, so you can always report i / totalNumberOfItems.
My application in WPF has to manage a series of windows. The main window manages the display of data acquired in real time. There are also other windows that allow you to tune parameters that change the displayed data. One of these windows consists of a slider and a textBox. The slide and the textBox bind to each other so that one updates the other. When the display is present, however, the textbox is not responsive while the slider is and this causes me a strong slowdown in the insertion of any parameters to tune.
In xaml:
<Slider Name="TAZSlider"
Grid.Column="0" Grid.Row="1"
Background="{x:Null}" mah:SliderHelper.EnableMouseWheel="MouseHover"
Orientation="Vertical"
Value="{Binding TAZValue}"
mah:SliderHelper.ThumbFillBrush="#FF9900"
mah:SliderHelper.ThumbFillHoverBrush="#faca82"
mah:SliderHelper.TrackValueFillBrush="#FF9900"
mah:SliderHelper.TrackValueFillPressedBrush="#FF9900"
mah:SliderHelper.TrackValueFillHoverBrush="#ff9900"
Maximum="{Binding TAZMax}"
Minimum="{Binding TAZMin}"
SmallChange="1"
IsSnapToTickEnabled="True"
ValueChanged="TAZSlider_ValueChanged"
AutoToolTipPlacement="TopLeft"
AutoToolTipPrecision="1"
TickFrequency="1"
ScrollViewer.PanningRatio="5"
ToolTipService.ToolTip="{Binding Path=Value, ElementName=TAZSlider}" ></Slider>
<TextBox Name="TAZTextBox" Grid.Column="0" Grid.Row="3" HorizontalAlignment="Center" Height="50" TextWrapping="Wrap" VerticalAlignment="Center" Width="100"
FontFamily="Segoe UI Symbol" FontSize="25" FontWeight="UltraBold" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Background="#FF707070" Foreground="#ececec"
Text="{Binding ElementName=TAZSlider, Path=Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" KeyDown="TAZTextBox_KeyDown" />
C#:
public int TAZMin
{
get { return this._mainParent.tazMinimLimits;}
set { this._mainParent.tazMinimLimits = value; }
}
public int TAZMax
{
get { return this._mainParent.tazMaximumLimits; }
set { this._mainParent.tazMaximumLimits = value; }
}
public int TAZValue
{
get { return this._mainParent.tazValue; }
set { this._mainParent.tazValue = value; }
}
private void TAZSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
if (!isFistStart)
{
System.Diagnostics.Debug.WriteLine("[DEBUG] : Set new value (int)TAZSlider.Value " + (int)TAZSlider.Value);
Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Render, new Action(() =>
{
this._mainParent.sinaps.dataManagerTools.DAQControl.SetAZ_Period((int)TAZSlider.Value);
}));
}
}
private void TAZTextBox_KeyDown(object sender, KeyEventArgs e)
{
System.Diagnostics.Debug.WriteLine("Debug TAZTextBox_KeyDown :" + e.Key);
Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Input, new Action(() => { TAZTextBox.Text = TAZTextBox.Text + "!"; }));
}
Without display everything works without having to associate the "TAZTextBox_KeyDown" event while with the display it does not. I tried to insert Dispatcher.Invoke (System.Windows.Threading.DispatcherPriority.Input, new Action (() => {TAZTextBox.Text = TAZTextBox.Text + "!";})) and that makes it distinctly responsive but sometimes it works (randomly) the keyboard entry works and therefore this causes me difficulties. Can anyone explain to me if there is a specific property of the textbox that makes keyboard input immediately responsive?
So, lets say I have STA thread running on background and I create a user control there.
How functional it is going to be? What are the limitations?
_workingThread = new Thread(() =>
{
//so far so good
var myControl = new MyCustomControl();
//what happens if i set DataContext? Will databinding work?
//It looks like it does, but I am not entirely sure.
myControl.DataContext = new MyViewModel();
//if databinding works, can I assume that at this point
//myControl's properties are already updated?
//what happens exactly if I invoke a delgate using Dispatcher property?
myControl.Dispatcher.Invoke(SomeMethod);
//or current dispatcher?
Dispatcher.CurrentDispatcher.BeginInvoke(SomeOtherMethod);
});
_workingThread.SetApartmentState(ApartmentState.STA);
_workingThread.Start();
To answer the question why: there is a component in .Net called XpsDocument which allows you to write visuals into xps file. I don't see a reason, why I should do it on UI thread.
Here is example of WPF app, which creates Window in new STA Thread. I don't see any problem with it. I printed out some things: Thread name, ThreadId and Counter (changes via INotifyPropertyChanged). Also I change stackPanelCounter's background from timer Dispatcher.BeginInvoke.
XAML:
<Window x:Class="WpfWindowInAnotherThread.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"
mc:Ignorable="d"
SizeToContent="WidthAndHeight" WindowStartupLocation="CenterScreen" Title="WPF: Windows and Threads">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto"/>
<RowDefinition />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Vertical">
<TextBlock Text="{Binding ThreadId, StringFormat='ThreadId: {0}'}" />
<TextBlock Text="{Binding ThreadName, StringFormat='ThreadName: {0}'}" />
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" Name="stackPanelCounter">
<TextBlock Text="Counter: " />
<TextBlock Text="{Binding Counter}" />
</StackPanel>
<StackPanel Grid.Row="2">
<Button Name="btnStartInNewThread" Content="Start window in new Thread"
Click="btnStartInNewThread_Click"/>
<Button Name="btnStartTheSameThread"
Content="Start window in the same Thread"
Click="btnStartTheSameThread_Click" />
</StackPanel>
</Grid>
</Window>
Code:
using System;
using System.ComponentModel;
using System.Threading;
using System.Windows;
using System.Windows.Media;
using System.Windows.Threading;
namespace WpfWindowInAnotherThread
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window, INotifyPropertyChanged
{
static int _threadNumber = 0;
readonly Timer _timer;
int _Counter;
public event PropertyChangedEventHandler PropertyChanged = delegate { };
public int ThreadId
{
get { return Thread.CurrentThread.ManagedThreadId; }
}
public string ThreadName
{
get { return Thread.CurrentThread.Name; }
}
public int Counter
{
get { return _Counter; }
set { _Counter = value; PropertyChanged(this, new PropertyChangedEventArgs("Counter")); }
}
public MainWindow()
{
DataContext = this;
_timer = new Timer((o) => {
Counter++;
MainWindow wnd = o as MainWindow;
wnd.Dispatcher.BeginInvoke(new Action<MainWindow>(ChangeStackPanelBackground), wnd);
}, this, 0, 200);
InitializeComponent();
}
private void btnStartTheSameThread_Click(object sender, RoutedEventArgs e)
{
MainWindow mainWnd = new MainWindow();
mainWnd.Show();
}
private void btnStartInNewThread_Click(object sender, RoutedEventArgs e)
{
Thread thread = new Thread(new ThreadStart(ThreadMethod));
thread.SetApartmentState(ApartmentState.STA);
thread.IsBackground = true;
thread.Start();
}
private static void ThreadMethod()
{
Thread.CurrentThread.Name = "MainWindowThread# " + _threadNumber.ToString();
Interlocked.Increment(ref _threadNumber);
MainWindow mainWnd = new MainWindow();
mainWnd.Show();
Dispatcher.Run();
}
private static void ChangeStackPanelBackground(MainWindow wnd)
{
Random rnd = new Random(Environment.TickCount);
byte[] rgb = new byte[3];
rnd.NextBytes(rgb);
wnd.stackPanelCounter.Background = new SolidColorBrush(Color.FromArgb(0xFF, rgb[0], rgb[1], rgb[2]));
}
}
}
I spent some time testing things out, and I think Clemens's comment was accurate. Key points are:
myControl.Dispatcher and Dispatcher.CurrentDispatcher are one and the same, both hold a reference to dispatcher of background thread. No surprises here.
In general controls will not behave correctly without dispatcher running, because Dispatcher.BeginInvoke calls will not be processed. You have two options. Either call Dispatcher.Run() on background thread and create your controls using invokes:
_backgroundDispatcher.BeginInvoke(new Action(() =>
{
var myControl = new MyCustomControl();
//do stuff
}));
or manually push dispatcher frame every time you want to process dispatcher queue and "refresh" your control. When it comes to building XPS pages, both approaches are viable.
Data bindings do work, even when control is created on background thread. However in some cases they are not applied instantly and you might have to wait for dispatcher to process it's queue.
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;