Related
So I originally was going to just throw marker classes on the canvas manually in code behind, but then I thought, hmm, I shouldn't really be throwing actual viewobjects into the code inside my view model. So then I thought, "Let me just have a collection and then add to it when i want the View to update."
However, I'm having issues. The problem I am having is that the line I wish to display is not appearing at the desired location.
My goal was to have a small thumb control that I had customized with a template that I could then place in a canvas and use the canvas.setLeft(double) function to set the position of the thumb. This worked fine when I placed it inside a canvas. However, as soon as I switched to the following itemscontrol, instead of displaying the line at the desired location, it always displays the line at coordinate x = 0.
When I call canvas.getleft() on the thumb control, it returns the desired coordinate. However, that is not where it is displaying. I have created a test case to narrow down outside factors and figure out what is going on.
<Application.Resources>
<Style x:Key="CanvasMarkerStyle" TargetType="{x:Type Comp:CanvasMarker}">
<Setter Property="OverridesDefaultStyle" Value="true"/>
<Setter Property="StrokeThickness" Value="3"/>
<Setter Property="Width" Value="3"/>
<Setter Property="Height" Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type Canvas}}, Path=ActualHeight}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Comp:CanvasMarker}">
<Line
Stroke="Orange"
StrokeThickness="{Binding RelativeSource={RelativeSource AncestorType={x:Type Comp:CanvasMarker}},
Path=StrokeThickness}"
X1="0"
Y1="0"
X2="0"
Y2="{Binding RelativeSource={RelativeSource AncestorType={x:Type Comp:CanvasMarker}}, Path=ActualHeight}"
/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Application.Resources>
and the Itemscontrol:
<ItemsControl
x:Name="ItemsControl"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Grid.ZIndex="1"
Background="Gray"
>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas
x:Name="MarkerCanvas"
SnapsToDevicePixels="false"
/>
</ItemsPanelTemplate>
<Comp:CanvasMarker
MarkerTime="50"
/>
</DataTemplate>
On Loaded, I take MarkerTime and Canvas.SetLeft(this, MarkerTime);
I feel like there is an obvious reason why I cannot use canvas.setleft. My guess is that it has something to do with how canvas manages its attached properties. I am guessing that it uses them during layout to place the child correctly. I think that maybe in my case where the attached.left property is changed inside an itemspanel, it doesn't prompt another layout or something. Anyway, an explanation or suggestions would be greatly appreciated.
I figured doing it this way was much more MVVM than just creating view items on a canvas manually from my view model with the correct properties that i desired.
but having a control so I can bind an items collection seems to be more of a headache than i anticipated.
public CanvasMarker()
{
this.Unloaded += new RoutedEventHandler(CanvasMarker_Unloaded);
this.Initialized += new EventHandler(CanvasMarker_Initialized);
this.Loaded += new RoutedEventHandler(CanvasMarker_Loaded);
this.DragStarted += new DragStartedEventHandler(CanvasMarker_DragStarted);
this.DragDelta += new DragDeltaEventHandler(CanvasMarker_DragDelta);
this.PreviewMouseDown += new System.Windows.Input.MouseButtonEventHandler(CanvasMarker_PreviewMouseDown);
this.Style = (Style)Application.Current.Resources["CanvasMarkerStyle"];
}
void CanvasMarker_Loaded(object sender, RoutedEventArgs e)
{
gvvm = Tag as GraphViewerViewModel;
var p = VisualTreeUtilities.FindParent<DependencyObject>(this);
var gp = VisualTreeUtilities.FindParent<DependencyObject>(p);
var ggp = VisualTreeUtilities.FindParent<DependencyObject>(gp);
var gggp = VisualTreeUtilities.FindParent<DependencyObject>(ggp);
ParentCanvas = VisualTreeUtilities.FindParent<Canvas>(this) as Canvas;
double MarkerHorizontalPositionInPixels = MarkerTime / gvvm.UnitsOfTimePerPixel;
SetMarker(MarkerHorizontalPositionInPixels);
}
void CanvasMarker_Initialized(object sender, EventArgs e)
{
}
void CanvasMarker_Unloaded(object sender, RoutedEventArgs e)
{
}
static void SetMarkerToNewPosition(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
CanvasMarker cm = d as CanvasMarker;
if (cm.IsLoaded)
{
double MarkerHorizontalPositionInPixels = cm.MarkerTime / cm.gvvm.UnitsOfTimePerPixel;
cm.SetMarker(MarkerHorizontalPositionInPixels);
if (cm.IsCurrentMarker)
{
cm.gvvm.MarkerExists = true;
cm.gvvm.CalculateValuesAtPrimaryMarker();
}
}
}
void CanvasMarker_PreviewMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
}
void CanvasMarker_DragDelta(object sender, DragDeltaEventArgs e)
{
double NewXPosition = XPositionInPixels + e.HorizontalChange;
SetMarker(NewXPosition);
}
void SetMarker(double PositionInPixels)
{
if (PositionInPixels < StrokeThickness / 2)
{
PositionInPixels = StrokeThickness / 2;
}
else if (PositionInPixels > ParentCanvas.ActualWidth)
{
PositionInPixels = ParentCanvas.ActualWidth;
}
Canvas.SetLeft(this, PositionInPixels);
double d = Canvas.GetLeft(this);
XPositionInPixels = PositionInPixels;
}
void CanvasMarker_DragStarted(object sender, DragStartedEventArgs e)
{
}
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;
I am making this app for windows phone 7, what I do is retrieve all the images from camera roll, saved pictures and other folder and display them in the listbox inside a wrap panel so they are displayed side by side....the thumbnail of the images is actually displayed hear.....
but as the number of images are increasing UI gets very slow and scrolling takes time...
I read many post and other question I think data virtualization or lazy loading is what I need but I am not understanding how can I use it, I saw the post from shawn oster and peter torr.....
I use a backgroundworker to load the images...
here's how...
void backroungWorker1_DoWork(object sender, DoWorkEventArgs e)
{
Dispatcher.BeginInvoke(() =>
{
foreach (string fileName in fileStorage.GetFileNames("images//*.*"))
{
if (fileName == null)
break;
string filepath = System.IO.Path.Combine("images", fileName);
try
{
using (IsolatedStorageFileStream imageStream = fileStorage.OpenFile(filepath, FileMode.Open))
{
var imageSource = PictureDecoder.DecodeJpeg(imageStream);
BitmapImage bitmapImage = new BitmapImage();
bitmapImage.SetSource(imageStream);
var item = new ImageToName { bmp = bitmapImage, FileName = fileName };
vltBitmapImage.Add(item);
imageStream.Dispose();
imageStream.Close();
}
}
catch
{
Exception x = new Exception();
}
}
if (vltBitmapImage.Count() != 0)
{
lone.Visibility = Visibility.Collapsed;
this.vaultbox.ItemsSource = vltBitmapImage;
}
else
lone.Visibility = Visibility.Visible;
});
}
any help is greatly appreciated.....
sorry for being a noob...
Try this sample from code project, it explain how it work and comes with a full sample project
See: Loading Data when the User scrolls to the end of the list
If you want to add a Lazy load to a listbox you must setup a listner for your list box and change the way you load data into your data model, so first lest set up at the XAML code for a listbox:
In your page resource add this style, and note the loaded event its include ("ScrollViewer_Loaded"):
...
<phone:PhoneApplicationPage.Resources>
<Style x:Key="BusinessListBoxStyle"
TargetType="ListBox">
<Setter Property="Background"
Value="Transparent" />
<Setter Property="Foreground"
Value="{StaticResource PhoneForegroundBrush}" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility"
Value="Disabled" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility"
Value="Auto" />
<Setter Property="BorderThickness"
Value="0" />
<Setter Property="BorderBrush"
Value="Transparent" />
<Setter Property="Padding"
Value="0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBox">
<ScrollViewer x:Name="scrollViewer"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
Foreground="{TemplateBinding Foreground}"
Padding="{TemplateBinding Padding}"
Loaded="ScrollViewer_Loaded">
<ItemsPresenter />
</ScrollViewer>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</phone:PhoneApplicationPage.Resources>
...
The add a reference to your style for the listbox, and bind your itemsSource to a list of items from your viewModel.
...
<ListBox x:Name="myList"
ItemsSource="{Binding myDataSource}"
Style="{StaticResource BusinessListBoxStyle}">
...
Next you have to set some code that loads data into the datamodel, when you reach the end of the list element in the list (the datamodel start loading more data into the mode, adds more items) Lazyloading!!
The way i normaly do this is by listing to the vertical offset of the listbox's scrollbar, and if its is about 1/4 from the edge i starts loading more items into the datamodel.
In the ScrollViewer loaded handler i set up at VertialOffset listener, by using the DependencyProperty, see code below:
public static readonly DependencyProperty ListVerticalOffsetProperty =
DependencyProperty.Register(
"ListVerticalOffset",
typeof(double),
typeof(MyPage),
new PropertyMetadata(new PropertyChangedCallback(OnListVerticalOffsetChanged))
);
private ScrollViewer _listScrollViewer;
private void ScrollViewer_Loaded(object sender, RoutedEventArgs e)
{
_listScrollViewer = sender as ScrollViewer;
Binding binding = new Binding();
binding.Source = _listScrollViewer;
binding.Path = new PropertyPath("VerticalOffset");
binding.Mode = BindingMode.OneWay;
this.SetBinding(ListVerticalOffsetProperty, binding);
}
public double ListVerticalOffset
{
get { return (double)this.GetValue(ListVerticalOffsetProperty); }
set { this.SetValue(ListVerticalOffsetProperty, value); }
}
private double _lastFetch;
private static void OnListVerticalOffsetChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
MyPage page = obj as MyPage;
ScrollViewer viewer = page._listScrollViewer;
if (viewer != null)
{
if (page._lastFetch < viewer.ScrollableHeight)
{
// Trigger within 1/4 the viewport.
if (viewer.VerticalOffset >= (viewer.ScrollableHeight - (viewer.ViewportHeight / 4)))
{
page._lastFetch = viewer.ScrollableHeight;
MyViewModel _tmpviewmodel = page.DataContext as MyViewModel;
if ((_tmpviewmodel != null) && (_tmpviewmodel.HasMoreItems))
_tmpviewmodel.GetMoreItems();
}
}
}
}
Note here i make use of a MyViewModel, that holds all the items the listbox i binded to, and has methods for load items from a database, the isolatedstore, the web or what ever your needs are.
You just have to find your way of only load a part of your data into the viewmodel. I your case i will first load a list of all the files you need to load (that is just to retreive the list from GetFileNames in the IsoLatedStore). Then mayby only loads 20 pics at the time!
I have my generic.xaml containing the following code:
<ControlTemplate TargetType="local:customVideoControl">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="600"/>
<RowDefinition Height="200"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="200"/>
</Grid.ColumnDefinitions>
<MediaElement x:Name="customMediaPlayer" Source="{TemplateBinding CustomMediaSource}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Height="{TemplateBinding Height}"
Width="{TemplateBinding Width}"
Grid.Row="0" Grid.ColumnSpan="3"
/>
<ToggleButton x:Name="playPauseBtn" Height="50" Width="50" Content="Pause" Grid.Row="1" Grid.Column="0"/>
<Button x:Name="prevBtn" Height="50" Width="50" Content="Prev" Grid.Row="1" Grid.Column="1"/>
<Button x:Name="nextBtn" Height="50" Width="50" Content="Next" Grid.Row="1" Grid.Column="2"/>
</Grid>
</ControlTemplate>
Now on applyTemplate , I am accessing the controls like below:
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
ToggleButton playPauseBtn = GetTemplateChild("playPauseBtn") as ToggleButton;
Button prevBtn= GetTemplateChild("prevBtn") as Button;
Button nextBtn = GetTemplateChild("nextBtn") as Button;
MediaElement customMediaPlayer = GetTemplateChild("customMediaPlayer") as MediaElement;
playPauseBtn.Checked += (obj, Args) =>
{
customMediaPlayer.Pause();
playPauseBtn.Content = "Play";
};
playPauseBtn.Unchecked += (obj, Args) =>
{
customMediaPlayer.Play();
playPauseBtn.Content = "Pause";
};
nextBtn.Click += (obj, Args) =>
{
customMediaPlayer.Source=new Uri(CustomMediaSource.ToString(),UriKind.RelativeOrAbsolute);
};
prevBtn.Click += (obj, Args) =>
{
customMediaPlayer.Source = new Uri(CustomMediaSource.ToString(), UriKind.RelativeOrAbsolute);
};
}
Now I want acccess the nextBtn, in the page where I am implementing like
CustomVideoControl myVControl=new CustomVideoControl();
This will create the instance of the control, but I want to do something on the click of
next and previous button, thta is present inside the CustomVideoControl in generic.xaml. Any help will be greatly appreciated.
Thanks,
Subhen
You just need to add a couple of events to your Control.
public event EventHandler MovedPrevious
public event EventHandler MovedNext
Now this are typically implemented like this:-
protected virtual void OnMovedPrevious(EventArgs e)
{
var handler = MovedPrevious;
if (handler != null)
handler(this, e);
}
protected virtual void OnMovedNext(EventArgs e)
{
var handler = MovedNext;
if (handler != null)
handler(this, e);
}
Now in your existing click events:-
nextBtn.Click += (obj, Args) =>
{
customMediaPlayer.Source=new Uri(CustomMediaSource.ToString(),UriKind.RelativeOrAbsolute); //No idea what this doing
OnMovedNext(EventArgs.Empty);
};
prevBtn.Click += (obj, Args) =>
{
customMediaPlayer.Source = new Uri(CustomMediaSource.ToString(), UriKind.RelativeOrAbsolute); //No idea what this is doing either
OnMovedPrevious(EventArgs.Empty);
};
Now in your consuming code you can do this sort of thing:-
CustomVideoControl myVControl=new CustomVideoControl();
myVControl.MovedNext += (s, args) => { /* deal with next */ };
myVControl.MovedPrevious += (s, args) => { /* deal with previous */ };
Sorry, but you're doing it wrong.
There's no good reason why you should have a reference to elements inside a DataTemplate IMO.
[...Read more at this forum post...]
You could create public events in your custom control, something like NextButtonClicked and PreviousButtonClicked.
I have a feeling, that you're trying to mimic EventSetter behaviour. If I'm right, please just take a look on this simple example:
<Style TargetType="{x:Type Button}" x:Key="SomeID">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Cursor" Value="Hand"></Setter>
<Setter Property="FontWeight" Value="Bold"></Setter>
</Trigger>
</Style.Triggers>
<EventSetter Event="MouseUp" Handler="DoSomething_Click"></EventSetter>
</Style>
This code assigns your custom event to some text block's regular action directly from XAML (you don't have to pollute your code behind with accessing controls' properties).
I hope this is helpful, but if not, please give me a shout.
Edit:
Sorry for not being perfectly clear (this was just a quickly pasted code snippet). Please have a look on a complete example:
Styles for your next/previous buttons:
<Style TargetType="{x:Type Button}" x:Key="PreviousButtonstyle">
<EventSetter Event="Click" Handler="OnMovedPrevious"></EventSetter>
</Style>
<Style TargetType="{x:Type Button}" x:Key="NextButtonstyle">
<EventSetter Event="Click" Handler="OnMovedNext"></EventSetter>
</Style>
Code behind:
public event EventHandler MovedPrevious;
public event EventHandler MovedNext;
protected void OnMovedPrevious(object sender, RoutedEventArgs e)
{
if (MovedPrevious != null)
{
MovedPrevious(this, e);
}
}
protected void OnMovedNext(object sender, RoutedEventArgs e)
{
if (MovedNext != null)
{
MovedNext(this, e);
}
}
Since now on you can access OnMovedNext and OnMovedPrevious directly from your control's handling conrol/whatever just as Anthony posted.
Sorry if my previous answer was confusing, but it supposed to be just an inspiration what to do :)
Edit:
I haven't noticed that this regards only Silverlight for which I apologize :) But, it works perfectly for WPF if you wish to try.
I have a listbox that is databound to a Collection of objects. The listbox is configured to display an identifier property of each object. I would like to show a tooltip with information specific to the item within the listbox that is being hovered over rather than one tooltip for the listbox as a whole.
I am working within WinForms and thanks to some helpful blog posts put together a pretty nice solution, which I wanted to share.
I'd be interested in seeing if there's any other elegant solutions to this problem, or how this may be done in WPF.
There are two main sub-problems one must solve in order to solve this problem:
Determine which item is being hovered over
Get the MouseHover event to fire when the user has hovered over one item, then moved the cursor within the listbox and hovered over another item.
The first problem is rather simple to solve. By calling a method like the following within your handler for MouseHover, you can determine which item is being hovered over:
private ITypeOfObjectsBoundToListBox DetermineHoveredItem()
{
Point screenPosition = ListBox.MousePosition;
Point listBoxClientAreaPosition = listBox.PointToClient(screenPosition);
int hoveredIndex = listBox.IndexFromPoint(listBoxClientAreaPosition);
if (hoveredIndex != -1)
{
return listBox.Items[hoveredIndex] as ITypeOfObjectsBoundToListBox;
}
else
{
return null;
}
}
Then use the returned value to set the tool-tip as needed.
The second problem is that normally the MouseHover event isn't fired again until the cursor has left the client area of the control and then come back.
You can get around this by wrapping the TrackMouseEvent Win32API call.
In the following code, the ResetMouseHover method wraps the API call to get the desired effect: reset the underlying timer that controls when the hover event is fired.
public static class MouseInput
{
// TME_HOVER
// The caller wants hover notification. Notification is delivered as a
// WM_MOUSEHOVER message. If the caller requests hover tracking while
// hover tracking is already active, the hover timer will be reset.
private const int TME_HOVER = 0x1;
private struct TRACKMOUSEEVENT
{
// Size of the structure - calculated in the constructor
public int cbSize;
// value that we'll set to specify we want to start over Mouse Hover and get
// notification when the hover has happened
public int dwFlags;
// Handle to what's interested in the event
public IntPtr hwndTrack;
// How long it takes for a hover to occur
public int dwHoverTime;
// Setting things up specifically for a simple reset
public TRACKMOUSEEVENT(IntPtr hWnd)
{
this.cbSize = Marshal.SizeOf(typeof(TRACKMOUSEEVENT));
this.hwndTrack = hWnd;
this.dwHoverTime = SystemInformation.MouseHoverTime;
this.dwFlags = TME_HOVER;
}
}
// Declaration of the Win32API function
[DllImport("user32")]
private static extern bool TrackMouseEvent(ref TRACKMOUSEEVENT lpEventTrack);
public static void ResetMouseHover(IntPtr windowTrackingMouseHandle)
{
// Set up the parameter collection for the API call so that the appropriate
// control fires the event
TRACKMOUSEEVENT parameterBag = new TRACKMOUSEEVENT(windowTrackingMouseHandle);
// The actual API call
TrackMouseEvent(ref parameterBag);
}
}
With the wrapper in place, you can simply call ResetMouseHover(listBox.Handle) at the end of your MouseHover handler and the hover event will fire again even when the cursor stays within the control's bounds.
I'm sure this approach, sticking all the code in the MouseHover handler must result in more MouseHover events firing than are really necessary, but it'll get the job done. Any improvements are more than welcome.
Using the MouseMove event, you can keep track of the index of the item that the mouse is over and store this in a variable that keeps its value between MouseMoves. Every time MouseMove is triggered, it checks to see if the index has changed. If so, it disables the tooltip, changes the tooltip text for this control, then re-activates it.
Below is an example where a single property of a Car class is shown in a ListBox, but then full information is shown when hovering over any one row. To make this example work, all you need is a ListBox called lstCars with a MouseMove event and a ToolTip text component called tt1 on your WinForm.
Definition of the car class:
class Car
{
// Main properties:
public string Model { get; set; }
public string Make { get; set; }
public int InsuranceGroup { get; set; }
public string OwnerName { get; set; }
// Read only property combining all the other informaiton:
public string Info { get { return string.Format("{0} {1}\nOwner: {2}\nInsurance group: {3}", Make, Model, OwnerName, InsuranceGroup); } }
}
Form load event:
private void Form1_Load(object sender, System.EventArgs e)
{
// Set up a list of cars:
List<Car> allCars = new List<Car>();
allCars.Add(new Car { Make = "Toyota", Model = "Yaris", InsuranceGroup = 6, OwnerName = "Joe Bloggs" });
allCars.Add(new Car { Make = "Mercedes", Model = "AMG", InsuranceGroup = 50, OwnerName = "Mr Rich" });
allCars.Add(new Car { Make = "Ford", Model = "Escort", InsuranceGroup = 10, OwnerName = "Fred Normal" });
// Attach the list of cars to the ListBox:
lstCars.DataSource = allCars;
lstCars.DisplayMember = "Model";
}
The tooltip code (including creating the class level variable called hoveredIndex):
// Class variable to keep track of which row is currently selected:
int hoveredIndex = -1;
private void lstCars_MouseMove(object sender, MouseEventArgs e)
{
// See which row is currently under the mouse:
int newHoveredIndex = lstCars.IndexFromPoint(e.Location);
// If the row has changed since last moving the mouse:
if (hoveredIndex != newHoveredIndex)
{
// Change the variable for the next time we move the mouse:
hoveredIndex = newHoveredIndex;
// If over a row showing data (rather than blank space):
if (hoveredIndex > -1)
{
//Set tooltip text for the row now under the mouse:
tt1.Active = false;
tt1.SetToolTip(lstCars, ((Car)lstCars.Items[hoveredIndex]).Info);
tt1.Active = true;
}
}
}
I think the best option, since your databinding your listbox to objects, would be to use
a datatemplate. So you could do something like this:
<ListBox Width="400" Margin="10"
ItemsSource="{Binding Source={StaticResource myTodoList}}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=TaskName}"
ToolTipService.ToolTip="{Binding Path=TaskName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Of course you'd replace the ItemsSource binding with whatever your binding source is, and the binding Path parts with whatever public property of the objects in the list you actually want to display.
More details available on msdn
You can use this simple code that uses the onMouseMove event of ListBox in WinForms:
private void ListBoxOnMouseMove(object sender, MouseEventArgs mouseEventArgs)
{
var listbox = sender as ListBox;
if (listbox == null) return;
// set tool tip for listbox
var strTip = string.Empty;
var index = listbox.IndexFromPoint(mouseEventArgs.Location);
if ((index >= 0) && (index < listbox.Items.Count))
strTip = listbox.Items[index].ToString();
if (_toolTip.GetToolTip(listbox) != strTip)
{
_toolTip.SetToolTip(listbox, strTip);
}
}
Of course you will have to init the ToolTip object in the constructor or some init function:
_toolTip = new ToolTip
{
AutoPopDelay = 5000,
InitialDelay = 1000,
ReshowDelay = 500,
ShowAlways = true
};
Enjoy!
Here is a Style that creates a group of RadioButtons by using a ListBox. All is bound for MVVM-ing. MyClass contains two String properties: MyName and MyToolTip. This will display the list of RadioButtons including proper ToolTip-ing. Of interest to this thread is the Setter for ToolTip near bottom making this an all Xaml solution.
Example usage:
ListBox Style="{StaticResource radioListBox}" ItemsSource="{Binding MyClass}" SelectedValue="{Binding SelectedMyClass}"/>
Style:
<Style x:Key="radioListBox" TargetType="ListBox" BasedOn="{StaticResource {x:Type ListBox}}">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Margin" Value="5" />
<Setter Property="Background" Value="{x:Null}" />
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="ListBoxItem" BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Grid Background="Transparent">
<RadioButton Focusable="False" IsHitTestVisible="False" IsChecked="{TemplateBinding IsSelected}" Content="{Binding MyName}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="ToolTip" Value="{Binding MyToolTip}" />
</Style>
</Setter.Value>
</Setter>
</Style>
Using onmouseover you can iterate through each item of the list and can show the ToolTip
onmouseover="doTooltipProd(event,'');
function doTooltipProd(e,tipObj)
{
Tooltip.init();
if ( typeof Tooltip == "undefined" || !Tooltip.ready ) {
return;
}
mCounter = 1;
for (m=1;m<=document.getElementById('lobProductId').length;m++) {
var mCurrent = document.getElementById('lobProductId').options[m];
if(mCurrent != null && mCurrent != "null") {
if (mCurrent.selected) {
mText = mCurrent.text;
Tooltip.show(e, mText);
}
}
}
}
Using title attribute, we can set tool tip for each list items in a list box.
Loop this for all the items in a list box.
ListItem li = new ListItem("text","key");
li.Attributes.Add("title","tool tip text");
Hope this helps.