Slow GridView Resize in UWP - xaml

To simplify the example let's suppose we just add a GridView in the Page:
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<GridView x:Name="MainGrid" />
</Grid>
And this is the code:
public MainPage()
{
this.InitializeComponent();
this.Loaded += MainPage_Loaded;
}
private async void MainPage_Loaded(object sender, RoutedEventArgs e)
{
var collection = new ObservableCollection<String>();
MainGrid.ItemsSource = collection;
await Dispatcher.RunIdleAsync(test =>
{
for (int i = 0; i < 20000; i++)
{
collection.Add(i.ToString());
}
});
}
A GridView is filled with that ObservableCollection, I do not complain about scrolling but about resizing and maximizing the Window makes a huge lag and even worse in low spec computers.
I have done the following improvements:
Play with the values of IncrementalLoadingThreshold & DataFetchSize
<GridView x:Name="MainGrid" IncrementalLoadingThreshold="100" DataFetchSize="3"/>
Sometimes goes better but not sure if this values works with an static collection.
Change the alignment to top and left to track the SizeChanged and adapt to the Grid.
This goes better but when I tap on maximize it has a large lag.
I have also tried to follow old examples like http://xurxodeveloper.blogspot.com.es/2014/03/scroll-infinito-en-windows-81-con-xaml.html but it does not add more items when scroll has arrived to the end. And the performance tips and articles I found are just for scrolling.
So In this static case, is there a technique or another collection source to improve the resizing lag?

Firstly create a new Observable Collection that allows you to AddRange. Here's what I have
public class ObservableCollection2<T> : ObservableCollection<T>
{
bool _suppressNotification = false;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (!_suppressNotification)
base.OnCollectionChanged(e);
// do nothing
}
public ObservableCollection2() : base() { }
public ObservableCollection2(IEnumerable<T> collection) : base(collection)
{ }
public void AddRange(IEnumerable<T> list)
{
this._suppressNotification = true;
if (list == null)
throw new ArgumentNullException("list");
int index = this.Count;
foreach (T item in list)
{
base.Items.Add(item);
}
this._suppressNotification = false;
OnPropertyChanged(new System.ComponentModel.PropertyChangedEventArgs("Count"));
OnPropertyChanged(new System.ComponentModel.PropertyChangedEventArgs("Items[]"));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, list.ToList(), index));
}
}
Next you don't instantiate the collection on Loaded rather do it in constructor
public MainPage()
{
this.InitializeComponent();
dataCol = new ObservableCollection2<String>();
}
ObservableCollection2<String> dataCol = null;
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
MainGrid.ItemsSource = dataCol;
List<stirng> tempList = new List<string>();
for (int i = 0; i < 20000; i++)
{
tempList .Add(i.ToString());
}
dataCol.AddRange(tempList);
}
In short you aren't triggering UI updates after every insert only when the collection has finished processing.. that means GridView gets update notification once rather than 2000 times

Related

Repeating brush or tile of image in WinUI 3

I'm finding it awfully hard to see how to simply cover a rectangular XAML element with repeating copies of a bitmap! I am using WinUI 3 with Windows App SDK. I would like to use the repeating image as a background element in my app.
It would seem to involve the composition API. Some tantalizing clues are given by Deiderik Krohls and by JetChopper ... however (a) there does not seem to be a stable released NuGet package for the required interface and (b) this seems like a very complicated way to do something that should be simple and (c) these solutions would seem to require extra work to integrate with WinUI 3 classes such as ImageSource and BitmapImage.
Any suggestions?
You can use a Direct2D effect, the Tile Effect for that. This effect is hardware accelerated. Microsoft provides a nuget called Win2D that enables that for WinUI: Microsoft.Graphics.Win2D
Once you have created a standard WinUI3 application project, add this nuget, and for this XAML:
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal">
<canvas:CanvasControl
x:Name="myCanvas"
Width="128"
Height="128"
CreateResources="myCanvas_CreateResources"
Draw="myCanvas_Draw" />
</StackPanel>
You can display a repetition of an image with a C# code like this:
public sealed partial class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
}
// handle canvas' CreateResources event for Win2D (Direct2D) resources
private void myCanvas_CreateResources(CanvasControl sender, CanvasCreateResourcesEventArgs args)
=> args.TrackAsyncAction(CreateResources(sender).AsAsyncAction());
// create all needed resources async (here a bitmap)
CanvasBitmap _canvasBitmap;
private async Task CreateResources(CanvasControl sender)
{
// this is my 32x32 image downloaded from https://i.stack.imgur.com/454HU.jpg?s=32&g=1
_canvasBitmap = await CanvasBitmap.LoadAsync(sender, #"c:\downloads\smo.jpg");
}
// handle canvas' Draw event
// check quickstart https://microsoft.github.io/Win2D/WinUI3/html/QuickStart.htm
private void myCanvas_Draw(CanvasControl sender, CanvasDrawEventArgs args)
{
// create an intermediate command list as a feed to the Direct2D effect
using var list = new CanvasCommandList(sender);
using var session = list.CreateDrawingSession();
session.DrawImage(_canvasBitmap);
// create the Direct2D effect (here Tile effect https://learn.microsoft.com/en-us/windows/win32/direct2d/tile)
using var tile = new TileEffect();
tile.Source = list;
// use image size as source rectangle
tile.SourceRectangle = _canvasBitmap.Bounds;
// draw the effect (using bitmap as input)
args.DrawingSession.DrawImage(tile);
}
}
Here is the result with my StackOverflow avatar as the bitmap source:
The image is 32x32 and the canvas is 128x128 so we have 4x4 tiles.
You can use the TilesBrush from the CommunityToolkit.
Install the CommunityToolkit.WinUI.UI.Media NuGet package and try this code:
<Window
x:Class="TileBrushes.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:toolkit="using:CommunityToolkit.WinUI.UI.Media"
mc:Ignorable="d">
<Grid ColumnDefinitions="*,*">
<Border Grid.Column="0">
<TextBlock Text="No tiles" />
</Border>
<Border Grid.Column="1">
<Border.Background>
<toolkit:TilesBrush TextureUri="ms-appx:///Assets/StoreLogo.png" />
</Border.Background>
<TextBlock Text="Tiles" />
</Border>
</Grid>
</Window>
The answer from #simon-mourier was the key for me in finally getting this done.
I created a TiledContentControl that has a ContentControl in front of the tiled background, and which reloads its bitmap image when the TileUriString property is changed (e.g. due to a binding).
There are also properties TileWidth, TileHeight to control the drawn size of the tile bitmap, as well as AlignRight and AlignBottom to make the bitmap align with the right edge or bottom edge instead of the left or top edge. The alignment parameters are useful to get a seamless continuity between two TiledContentControls that are right next to each other.
I am providing this back to the community with thanks for all of the help I've gotten on various coding challenges in the past. Note: I have done some basic testing but not extensive testing.
The key nuget packages used are Microsoft.Graphics.Win2D 1.0.4 and Microsoft.WindowsAppSDK 1.2. There are some interesting coding challenges that I discuss in a comment in the code. For example the need to prevent memory leakage when subscribing to Win2D C++ events from WinUI3 C# code.
Here is TiledContentControl.xaml:
<UserControl
x:Class="Z.Framework.TiledContentControl"
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:win2d="using:Microsoft.Graphics.Canvas.UI.Xaml"
mc:Ignorable="d"
Padding="0"
>
<Grid
RowDefinitions="*"
ColumnDefinitions="*"
>
<win2d:CanvasControl
x:Name="CanvasControl"
Grid.Row="0"
Grid.Column="0"
>
</win2d:CanvasControl>
<ContentPresenter
Name="ContentPresenter"
Grid.Row="0"
Grid.Column="0"
Background="Transparent"
Foreground="{x:Bind Foreground, Mode=OneWay}"
HorizontalContentAlignment="{x:Bind HorizontalContentAlignment, Mode=OneWay}"
VerticalContentAlignment="{x:Bind VerticalContentAlignment, Mode=OneWay}"
Padding="{x:Bind Padding, Mode=OneWay}"
Content="{x:Bind Content, Mode=OneWay}"
>
</ContentPresenter>
</Grid>
</UserControl>
Here is TiledContentControl.xaml.cs:
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Brushes;
using Microsoft.Graphics.Canvas.UI;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Markup;
using System;
using System.Diagnostics;
using System.Numerics;
using System.Threading.Tasks;
using Windows.Foundation;
namespace Z.Framework
{
/// <summary>
/// A control that has a tiled (repeating) bitmap background behind a content control.
///
/// Setting the TileUriString will change the tiled bitmap. Setting the drawing parameters
/// (TileWidth, TileHeight, AlignRight, AlignBottom) will scale the bitmap or offset it so
/// that it is right or bottom aligned.
/// </summary>
[ContentProperty(Name="Content")]
public sealed partial class TiledContentControl : UserControl
{
#region Discussion
// There are a number of necessary objectives to achieve the Win2D tiling with post-Load updates.
// Goal: to trigger an async load-resources when a resource-related property of the control
// changes. This is accomplished by calling StartLoadingResources when the TileUriString changes.
// Goal: cancel any resource loads that are in progress when the new load is requested.
// This is done in StartNewLoadResourcesTaskAndCleanupOldTaskAsync.
// To do it, one must store the resource-loading task (LoadResourcesTask).
// Goal: to store the resources that have been loaded, and dispose them timely.
// The LoadResourcesTask contains the loaded resources in the Result property.
// They are kept around indefinitely, except if we start a new resource load task
// then any resources in the old load task are disposed. Also, when loading several
// resources, if one of the resource loads fails then we dispose of the others.
// The CanvasResourcesRecord and LoadResourcesAsync provide a generalizable way of
// storing resources in the task result.
// Goal: make sure that any exceptions from resource creation are thrown to Win2D, so that
// Win2D can handle device-lost events (which includes Win2D triggering a new CreateResources).
// It is accomplished by only throwing load-resource exceptions from the Win2d draw handler.
// Goal: prevent Draw from being called before resources are loaded. Resource loads that are
// triggered by Win2D go through the CreateResources event handler, allowing the use of
// CanvasCreateResourcesEventArgs.TrackAsyncAction which will postpone the Draw call -- not
// until the resources are loaded but at least while the load task is started. A Draw
// callback may then occur before the load completes, but then when the load completes
// it will invalidate the CanvasControl and another Draw callback will occur.
// It does not appear to be necessary from a Win2D perspective to prevent Draw calls
// while subsequent (post-CreateResources) resource loads are being done.
// Goal: to prevent memory leaks due to .NET not being able to detect the reference cycle
// between the main control and the CanvasControl. This is accomplished by only subscribing
// to CanvasControl events while the main control is loaded.
// References:
// https://microsoft.github.io/Win2D/WinUI2/html/M_Microsoft_Graphics_Canvas_UI_CanvasCreateResourcesEventArgs_TrackAsyncAction.htm
// https://stackoverflow.com/questions/74527783/repeating-brush-or-tile-of-image-in-winui-3-composition-api
// https://microsoft.github.io/Win2D/WinUI2/html/RefCycles.htm
// https://english.r2d2rigo.es/
// https://microsoft.github.io/Win2D/WinUI3/html/M_Microsoft_Graphics_Canvas_UI_CanvasCreateResourcesEventArgs_TrackAsyncAction.htm
// https://learn.microsoft.com/en-us/windows/win32/direct2d/tile
#endregion
#region ctor
public TiledContentControl()
{
this.InitializeComponent();
this.Loaded += this.TiledContentControl_Loaded; // OK, same lifetime
this.Unloaded += this.TiledContentControl_Unloaded; // OK, same lifetime
}
private void TiledContentControl_Loaded(object sender, RoutedEventArgs e)
{
this.CanvasControl.Draw += this.CanvasControl_Draw; // OK, matched in Unloaded
this.CanvasControl.CreateResources += this.CanvasControl_CreateResources;
}
private void TiledContentControl_Unloaded(object sender, RoutedEventArgs e)
{
this.CanvasControl.Draw -= this.CanvasControl_Draw;
this.CanvasControl.CreateResources -= this.CanvasControl_CreateResources;
}
#endregion
#region CanvasResourcesRecord, LoadResourcesAsync, LoadResourcesTask
private record class CanvasResourcesRecord(
CanvasBitmap TileBitmap,
CanvasImageBrush TileBrush
): IDisposable
{
public void Dispose()
{
this.TileBitmap.Dispose();
this.TileBrush.Dispose();
}
}
static private async Task<CanvasResourcesRecord> LoadResourcesAsync(CanvasControl canvasControl, string tileUriString)
{
object[] resources = new object[2];
try {
Uri tileUri = new Uri(tileUriString);
Task<CanvasBitmap> loadTileBitmap = CanvasBitmap.LoadAsync(canvasControl, tileUri).AsTask();
CanvasBitmap tileBitmap = await loadTileBitmap;
resources[0] = tileBitmap;
CanvasImageBrush tileBrush = new CanvasImageBrush(canvasControl, tileBitmap);
tileBrush.ExtendX = CanvasEdgeBehavior.Wrap;
tileBrush.ExtendY = CanvasEdgeBehavior.Wrap;
resources[1] = tileBrush;
} catch {
// Cleanup from partial/incomplete creation
foreach (object? resource in resources) {
(resource as IDisposable)?.Dispose();
}
throw;
}
canvasControl.Invalidate(); // now that resources are loaded, we trigger an async Draw.
return new CanvasResourcesRecord(
TileBitmap: (CanvasBitmap)resources[0],
TileBrush: (CanvasImageBrush)resources[1]
);
}
private Task<CanvasResourcesRecord>? LoadResourcesTask {
get { return this._loadResourcesTask; }
set { this._loadResourcesTask = value; }
}
private Task<CanvasResourcesRecord>? _loadResourcesTask;
#endregion
#region CanvasControl_CreateResources
private void CanvasControl_CreateResources(CanvasControl sender, CanvasCreateResourcesEventArgs args)
{
Debug.Assert(sender == this.CanvasControl);
args.TrackAsyncAction(this.StartNewLoadResourcesTaskAndCleanupOldTaskAsync().AsAsyncAction());
}
#endregion
#region StartLoadingResources, StartNewLoadResourcesTaskAndCleanupOldTaskAsync
private void StartLoadingResources()
{
if (this.CanvasControl.IsLoaded) {
Task _ = this.StartNewLoadResourcesTaskAndCleanupOldTaskAsync();
}
}
private async Task StartNewLoadResourcesTaskAndCleanupOldTaskAsync()
{
// Start new task, if the necessary input properties are available.
string? tileUriString = this.TileUriString;
Task<CanvasResourcesRecord>? oldTask = this.LoadResourcesTask;
if (tileUriString != null) {
this.LoadResourcesTask = LoadResourcesAsync(this.CanvasControl, tileUriString);
} else {
this.LoadResourcesTask = null;
}
// Cleanup old task.
if (oldTask != null) {
oldTask.AsAsyncAction().Cancel();
try {
await oldTask;
} catch {
// ignore exceptions from the cancelled task
} finally {
if (oldTask.IsCompletedSuccessfully) {
oldTask.Result.Dispose();
}
}
}
}
#endregion
#region CanvasControl_Draw, ActuallyDraw
private void CanvasControl_Draw(CanvasControl sender, CanvasDrawEventArgs args)
{
Debug.Assert(sender == this.CanvasControl);
if (!this.DrawingParameters.AreFullyDefined) { return; }
if (!this.DrawingParameters.AreValid) { throw new InvalidOperationException($"Invalid drawing parameters (typically width or height)."); }
Task<CanvasResourcesRecord>? loadResourcesTask = this.LoadResourcesTask;
if (loadResourcesTask == null) { return; }
if (loadResourcesTask.IsCompletedSuccessfully) {
CanvasResourcesRecord canvasResources = loadResourcesTask.Result;
this.ActuallyDraw( args, canvasResources);
} else if (loadResourcesTask.IsFaulted) {
// Throw exceptions to Win2D, for example DeviceLostException resulting in new CreateResoures event
loadResourcesTask.Exception?.Handle(e => throw e);
} else {
return;
}
}
private void ActuallyDraw( CanvasDrawEventArgs args, CanvasResourcesRecord canvasResources)
{
Debug.Assert(this.DrawingParameters.AreFullyDefined && this.DrawingParameters.AreValid);
Debug.Assert(this.DrawingParameters.AlignRight != null && this.DrawingParameters.AlignBottom != null);
CanvasControl canvasControl = this.CanvasControl;
float scaleX = (float)(this.DrawingParameters.TileWidth / canvasResources.TileBitmap.Bounds.Width);
float scaleY = (float)(this.DrawingParameters.TileHeight / canvasResources.TileBitmap.Bounds.Height);
float translateX = ((bool)this.DrawingParameters.AlignRight) ? (float)((canvasControl.RenderSize.Width % this.DrawingParameters.TileWidth) - this.DrawingParameters.TileWidth) : (float)0;
float translateY = ((bool)this.DrawingParameters.AlignBottom) ? (float)((canvasControl.RenderSize.Height % this.DrawingParameters.TileHeight) - this.DrawingParameters.TileHeight) : (float)0;
Matrix3x2 transform = Matrix3x2.CreateScale( scaleX, scaleY);
transform.Translation = new Vector2(translateX, translateY);
canvasResources.TileBrush.Transform = transform;
Rect rectangle = new Rect(new Point(), canvasControl.RenderSize);
args.DrawingSession.FillRectangle(rectangle, canvasResources.TileBrush);
}
#endregion
#region Content
new public UIElement? Content {
get { return (UIElement?)this.GetValue(ContentProperty); }
set { this.SetValue(ContentProperty, value); }
}
new public static DependencyProperty ContentProperty { get; } = DependencyProperty.Register(nameof(TiledContentControl.Content), typeof(UIElement), typeof(TiledContentControl), new PropertyMetadata(default(UIElement)));
#endregion
#region TileUriString
public string? TileUriString {
get { return (string?)this.GetValue(TileUriStringProperty); }
set { this.SetValue(TileUriStringProperty, value); }
}
public static readonly DependencyProperty TileUriStringProperty = DependencyProperty.Register(nameof(TiledContentControl.TileUriString), typeof(string), typeof(TiledContentControl), new PropertyMetadata(default(string), new PropertyChangedCallback(OnTileUriStringChanged)));
private static void OnTileUriStringChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
TiledContentControl #this = (TiledContentControl)sender;
#this.StartLoadingResources();
}
#endregion
#region TileWidth, TileHeight, AlignRight, AlignBottom; OnDrawingParameterChanged, DrawingParametersRecord, DrawingParameters
public double TileWidth {
get { return (double)this.GetValue(TileWidthProperty); }
set { this.SetValue(TileWidthProperty, value); }
}
public static readonly DependencyProperty TileWidthProperty = DependencyProperty.Register(nameof(TileWidth), typeof(double), typeof(TiledContentControl), new PropertyMetadata(double.NaN, new PropertyChangedCallback(OnDrawingParameterChanged)));
public double TileHeight {
get { return (double)this.GetValue(TileHeightProperty); }
set { this.SetValue(TileHeightProperty, value); }
}
public static readonly DependencyProperty TileHeightProperty = DependencyProperty.Register(nameof(TileHeight), typeof(double), typeof(TiledContentControl), new PropertyMetadata(double.NaN, new PropertyChangedCallback(OnDrawingParameterChanged)));
public bool? AlignRight {
get { return (bool?)this.GetValue(AlignRightProperty); }
set { this.SetValue(AlignRightProperty, value); }
}
public static readonly DependencyProperty AlignRightProperty = DependencyProperty.Register(nameof(AlignRight), typeof(bool?), typeof(TiledContentControl), new PropertyMetadata(default(bool?), new PropertyChangedCallback(OnDrawingParameterChanged)));
public bool? AlignBottom {
get { return (bool?)this.GetValue(AlignBottomProperty); }
set { this.SetValue(AlignBottomProperty, value); }
}
public static readonly DependencyProperty AlignBottomProperty = DependencyProperty.Register(nameof(AlignBottom), typeof(bool?), typeof(TiledContentControl), new PropertyMetadata(default(bool?), new PropertyChangedCallback(OnDrawingParameterChanged)));
private static void OnDrawingParameterChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
TiledContentControl #this = (TiledContentControl)sender;
#this.DrawingParameters = new DrawingParametersRecord(#this.TileWidth, #this.TileHeight, #this.AlignRight, #this.AlignBottom);
#this.CanvasControl.Invalidate(); // trigger an async redraw using the new parameters.
}
private record struct DrawingParametersRecord(
double TileWidth,
double TileHeight,
bool? AlignRight,
bool? AlignBottom
)
{
public bool AreFullyDefined => !double.IsNaN(this.TileWidth) && !double.IsNaN(this.TileHeight) && this.AlignBottom != null && this.AlignRight != null;
public bool AreValid => this.TileWidth > 0 && this.TileHeight > 0;
}
private DrawingParametersRecord DrawingParameters { get; set; }
#endregion
}
}

ListView flickering when updating binding collection

I am working on a Windows 10 Universal app and see some flickering issues when I use a ListView in my app. My ListView is using x:Bind to bind to an ObservableCollection in my View Model.
When user performs some actions, or a background update occurs, I do some processing that requires the ObservableCollection to be refreshed.
private ObservableCollection<Item> UIItems = new ObservableCollection<Item>();
private bool IsUpdating = false;
private void UpdateUIProperties(List<Item> newItems)
{
DispatcherHelper.CheckBeginInvokeOnUI(() =>
{
IsUpdating = true;
UIItems.Clear();
foreach (var item in newItems)
{
if (item.IsVisible)
{
UIItems.Add(item);
}
}
IsUpdating = false;
});
}
After this code gets executed, the ListView flickers and then the Scrollviewer goes all the way to the top. Is there any way to prevent this and have the ListView's ScrollViewer stay at its original offset?
A solution that seem to work for me is to bind the Itemsource to an Observable collection and then have another collection that contains the items that you want to add. Have the Item in the collection implement the interface below. When you want to update the collection use the MergeCollection method to make sure the items in the collection are preserved, but they have the new config.
public interface IConfigureFrom<T>
{
void ConfigureFrom(T other);
}
public static void MergeCollection<T>(ICollection<T> source, ICollection<T> dest) where T : IConfigureFrom<T>, new()
{
// First remove entries at the bottom of the dest list that are no longer there
if (dest.Count > source.Count)
{
for (int i = dest.Count - 1; i >= source.Count; i--)
{
var coll = dest as Collection<T>;
if (coll != null)
{
coll.RemoveAt(i);
}
else
{
dest.Remove(dest.Last());
}
}
}
// reconfigure existing entries with the new configureation
var sourecList = source.ToList();
var destList = dest.ToList();
for (int i = dest.Count - 1; i >= 0; i--)
{
var target = destList[i];
var config = sourecList[i];
target.ConfigureFrom(config);
}
// add new entries at the end and configure them from the source list
for (int i = dest.Count; i < source.Count; i++)
{
T newItem = new T();
newItem.ConfigureFrom(sourecList[i]);
dest.Add(newItem);
}
}
When changing all items in your ListView, it is usually better to just swap the whole ItemsSource.
Just set:
UIItems = new List<...>(your data);
And have it fire OnNotifyPropertyChanged of course.

What is the correct property name to notify when binding to an element in an Observable collection directly?

I have a binding of the following form in XAML,
Title="{Binding SelectedNewsItems[0].Title}"
Note that it refers to a particular element in the SelectedNewsItems which is an ObservableCollection. (I have a collection of nine buttons of various sizes, each styled, and sized differently and so a more standard ListView is not appropriate.)
When I reassign SelectedNewsItems I raise a PropertyChanged event for SelectedNewsItems, however, this does not appear to cause the bindings to update for the individual bound elements and their properties. I have tried the following,
public ObservableCollection<NewsItem> _selectedNewsItems;
public ObservableCollection<NewsItem> SelectedNewsItems
{
get
{
return this._selectedNewsItems;
}
set
{
if (this._selectedNewsItems != value)
{
this._selectedNewsItems = value;
this.NotifyPropertyChanged();
for (int i = 0; i < this._selectedNewsItems.Count; i++)
{
this.NotifyPropertyChanged(String.Format("SelectedNewsItems[{0}].Content", i));
this.NotifyPropertyChanged(String.Format("SelectedNewsItems[{0}].Title", i));
this.NotifyPropertyChanged(String.Format("SelectedNewsItems[{0}].Id", i));
this.NotifyPropertyChanged(String.Format("SelectedNewsItems[{0}].Image", i));
}
}
}
}
Hmm, I cannot exacly say where your code is wrong (as I see only part of it), but maybe you haven't set your DataContex or something else. For the purpose of research I've made simple example, which works quite fine. Take a look at it and maybe it will help you:
In Xaml:
<Button x:Name="first" VerticalAlignment="Top" Content="{Binding SelectedNewsItems[0].Name}" Grid.Row="0"/>
<Button x:Name="second" VerticalAlignment="Center" Content="{Binding SelectedNewsItems[1].Name}" Grid.Row="1"/>
In code behind (I put all the code - yeah quite a lot of, but I cannot guess what is wrong with your code):
public class NewsItem
{
private string name;
public string Name
{
get { return name; }
set { name = value; }
}
}
public partial class MainPage : PhoneApplicationPage, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void RaiseProperty(string property = null)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(property));
}
private ObservableCollection<NewsItem> _selectedNewsItems = new ObservableCollection<NewsItem>();
public ObservableCollection<NewsItem> SelectedNewsItems
{
get
{
return this._selectedNewsItems;
}
set
{
if (this._selectedNewsItems != value)
{
this._selectedNewsItems = value;
this.RaiseProperty();
for (int i = 0; i < this._selectedNewsItems.Count; i++)
{
this.RaiseProperty(String.Format("SelectedNewsItems[{0}].Name", i));
}
}
}
}
public MainPage()
{
NewsItem firstT = new NewsItem() { Name = "First" };
NewsItem secondT = new NewsItem() { Name = "Second" };
SelectedNewsItems.Add(firstT);
SelectedNewsItems.Add(secondT);
InitializeComponent();
this.DataContext = this;
first.Click += first_Click;
second.Click += second_Click;
}
private void first_Click(object sender, RoutedEventArgs e)
{
NewsItem change = new NewsItem() { Name = "Changed by First" };
SelectedNewsItems[1] = change;
}
private void second_Click(object sender, RoutedEventArgs e)
{
NewsItem change = new NewsItem() { Name = "Changed by Second" };
SelectedNewsItems[0] = change;
}
}
As I click on buttons the bindigs work, so maybe it will help you.

Is this the efficient way to display list of timers constantly updating the UI?

I am displaying a list of timers which constantly update the UI (every one second) to show proper time.
Is this the efficient way ? How this process can be improved from performance point of view ?
I am using MVVMLight toolkit for windows phone.
XAML code:
<ListBox ItemsSource="{Binding TimersCollection}"
ItemTemplate="{StaticResource SingleItemTemplate}"/>
Here is my simple itemtemplate code,It also has Pause, Add Minute button but removed from here for simplicity:
<phone:PhoneApplicationPage.Resources>
<DataTemplate x:Key="SingleItemTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding CurrentTime.Hours}"/>
<TextBlock Text="H"></TextBlock>
<TextBlock Text="{Binding CurrentTime.Minutes}" />
<TextBlock Text="M"></TextBlock>
<TextBlock Text="{Binding CurrentTime.Seconds}" />
<TextBlock Text="S"></TextBlock>
</StackPanel>
</DataTemplate>
</phone:PhoneApplicationPage.Resources>
Here is my ViewModel code which is injected in view:
public class Page1VM : ViewModelBase
{
private ObservableCollection<MyTimer> _timersCollection = new ObservableCollection<MyTimer>();
public Page1VM()
{
// sample code to simulate collection of timers
for (int i = 1; i < 5; i++)
{
var t = new MyTimer();
t.TotalTimeSpan = new TimeSpan(0, i, 0);
_timersCollection.Add(t);
t.Start();
}
}
public IList<MyTimer> TimersCollection
{
get { return _ghatikatimerscoll; }
}
}
Here is ITimer Interace
public interface ITimer
{
bool Start();
bool Stop();
bool IsRunning { get; set; }
void AddMinute();
}
Its implementation
public class MyTimer : ViewModelBase, ITimer
{
public TimeSpan TotalTimeSpan { private get; set; }
private readonly DispatcherTimer _myDispatcherTimer;
private TimeSpan _startTime;
public bool IsRunning { get; set; }
public MyTimer()
{
_myDispatcherTimer = new DispatcherTimer { Interval = new TimeSpan(0, 0, 1) };
_myDispatcherTimer.Tick += _myDispatcherTimer_Tick;
}
private TimeSpan _currentTime;
public TimeSpan CurrentTime
{
get
{
return _currentTime;
}
set
{
_currentTime = value;
RaisePropertyChanged("CurrentTime");
}
}
void _myDispatcherTimer_Tick(object sender, EventArgs e)
{
if (_myDispatcherTimer.IsEnabled)
{
var currenttime = TotalTimeSpan.Add(new TimeSpan(0, 0, 1)) - (DateTime.Now.TimeOfDay - _startTime);
CurrentTime = currenttime;
}
}
public bool Start()
{
_startTime = DateTime.Now.TimeOfDay;
if (_currentTime.TotalSeconds != 0)
{
// resuming after paused
TotalTimeSpan = CurrentTime;
}
IsRunning = true;
_myDispatcherTimer.Start();
return true;
}
public bool Stop()
{
_myDispatcherTimer.Stop();
IsRunning = false;
return true;
}
public void AddMinute()
{
TotalTimeSpan = TotalTimeSpan.Add(new TimeSpan(0, 1, 1));
}
}
Basically, I am displaying a collection of Timers on screen which updates itself. Each item in list has its own DispatcherTimer. User an click "Pause" button for each item to pause that particular timer. User can also click on "Add Minute" button which adds 1 minute to the particular item in collection.
Is this method efficient to update the UI constantly?
Rather than having one timer per item, you can declare one global dispatcher timer that will update every item every second (actually, you should set a delay lower than a second or you may see some items skipping a second). It will be much more efficient than using x dispatcher timers. You'd have to put it in the ViewModel and iterate with a foreach loop. You could also create a wrapper around the timer, define an event that will be triggered every second, and have all the items subscribe to that event, but I don't think it's worth the trouble.

How do I hide a PivotItem?

I have a Page with an Pivot-control and in some cases I don't want to show a particular PivotItem.
Setting the Visibility to collapsed doesn't seem to affect it at all.
Any suggestions?
you should be able to remove or add PivotItems dynamically in your Pivot by using the respective collection methods on Pivot.Items .
Let me know if this doesn't work for your scenario.
I've created a custom behavior for showing/hiding pivot item
Usage:
< i:Interaction.Behaviors>
< common:HideablePivotItemBehavior Visible="{Binding variable}" />
</ i:Interaction.Behaviors >
Code:
/// <summary>
/// Behavior which enables showing/hiding of a pivot item`
/// </summary>
public class HideablePivotItemBehavior : Behavior<PivotItem>
{
#region Static Fields
public static readonly DependencyProperty VisibleProperty = DependencyProperty.Register(
"Visible",
typeof(bool),
typeof(HideablePivotItemBehavior),
new PropertyMetadata(true, VisiblePropertyChanged));
#endregion
#region Fields
private Pivot _parentPivot;
private PivotItem _pivotItem;
private int _previousPivotItemIndex;
private int _lastPivotItemsCount;
#endregion
#region Public Properties
public bool Visible
{
get
{
return (bool)this.GetValue(VisibleProperty);
}
set
{
this.SetValue(VisibleProperty, value);
}
}
#endregion
#region Methods
protected override void OnAttached()
{
base.OnAttached();
this._pivotItem = AssociatedObject;
}
private static void VisiblePropertyChanged(DependencyObject dpObj, DependencyPropertyChangedEventArgs change)
{
if (change.NewValue.GetType() != typeof(bool) || dpObj.GetType() != typeof(HideablePivotItemBehavior))
{
return;
}
var behavior = (HideablePivotItemBehavior)dpObj;
var pivotItem = behavior._pivotItem;
// Parent pivot has to be assigned after the visual tree is initialized
if (behavior._parentPivot == null)
{
behavior._parentPivot = (Pivot)behavior._pivotItem.Parent;
// if the parent is null return
if (behavior._parentPivot == null)
{
return;
}
}
var parentPivot = behavior._parentPivot;
if (!(bool)change.NewValue)
{
if (parentPivot.Items.Contains(behavior._pivotItem))
{
behavior._previousPivotItemIndex = parentPivot.Items.IndexOf(pivotItem);
parentPivot.Items.Remove(pivotItem);
behavior._lastPivotItemsCount = parentPivot.Items.Count;
}
}
else
{
if (!parentPivot.Items.Contains(pivotItem))
{
if (behavior._lastPivotItemsCount >= parentPivot.Items.Count)
{
parentPivot.Items.Insert(behavior._previousPivotItemIndex, pivotItem);
}
else
{
parentPivot.Items.Add(pivotItem);
}
}
}
}
#endregion
}
You can remove the pivot item from the parent pivot control
parentPivotControl.Items.Remove(pivotItemToBeRemoved);
Removing PivotItems is easy, but if you want to put them back afterwards I've found that the headers get messed up and start overlapping each other. This also happens if you set the Visibility of a header to Collapsed and then later make it Visible again.
So I solved my particular problem by setting the opacity of each unwanted PivotItem (and its header) to 0.
PivotItem p = (PivotItem)MainPivot.Items.ToList()[indexToHide];
p.Opacity = 0;
((UIElement)p.Header).Opacity = 0;
However, this leaves gaps where the missing PivotItems are.
For me, the gaps were not a problem because I only want to remove items at the end of my PivotItemList, so I get some whitespace between the last and first PivotItems. The problem was, I was still able to swipe to a hidden PivotItem. In order to fix this, I overrode Pivot.SelectionChanged() so that whenever the user swipes to a hidden PivotItem, the code moves on to the next item instead. I had to use a DispatchTimer from within SelectionChanged() and actually move to the next PivotItem from the DispatchTimer callback, since you have to be in the UI thread to change PivotItem.SelectedIndex.
private void MainPivot_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
t.Stop(); //t is my DispatchTimer, set to 100ms
if (MainPivot.SelectedIndex >= mFirstHiddenPivotItemIndex)
{
//move to the first or last PivotItem, depending on the current index
if (mCurrentSelectedPivotItemIndex == 0)
mPivotItemToMoveTo = mFirstHiddenPivotItemIndex - 1;
else
mPivotItemToMoveTo = 0;
t.Start();
}
mCurrentSelectedPivotItemIndex = MainPivot.SelectedIndex;
}
private void dispatcherTimer_Tick(object sender, EventArgs e)
{
MainPivot.SelectedIndex = mPivotItemToMoveTo;
t.Stop();
}
foreach (PivotItem item in MyPivot.Items.ToList())
{
if (item.Visibility == Visibility.Collapsed)
MyPivot.Items.Remove(item);
}
Setting IsLocked property to true will make all other Pivot items to disappear except the current pivot item.
But this will not hide one particular pivot item of our choice.
To elaborate on the solution of adding/removing pivotItems, rather than hiding them.
Let's say we want the pivotItem to be initially invisible, and appear only on a certain event.
mainPivot.Items.Remove(someTab);
Then to add it again,
if (!mainPivot.Items.Cast<PivotItem>().Any(p => p.Name == "someTab"))
{
mainPivot.Items.Insert(1,someTab);
}
I've used Insert rather than add to control the position where the tab appears.
You have to ensure you don't add the same tab twice, which is the reason for the if statement.
I've modified the Bajena behavior to improve it, solving the issue with losing the original position of the PivotItem when showing/hiding repeteadly and the issue when parentpivot control is null (not initialized yet).
Notice that this behavior must be attached to the Pivot, not to the PivotItem.
Code:
public class PivotItemHideableBehavior : Behavior<Pivot>
{
private Dictionary<PivotItem, int> DictionaryIndexes { get; set; }
public static readonly DependencyProperty VisibleProperty = DependencyProperty.Register(
"Visible",
typeof(bool),
typeof(PivotItemHideableBehavior),
new PropertyMetadata(true, VisiblePropertyChanged));
public static readonly DependencyProperty PivotItemProperty = DependencyProperty.Register(
"PivotItem",
typeof(PivotItem),
typeof(PivotItemHideableBehavior),
new PropertyMetadata(null));
public bool Visible
{
get { return (bool)GetValue(VisibleProperty); }
set { SetValue(VisibleProperty, value); }
}
public PivotItem PivotItem
{
get { return (PivotItem)GetValue(PivotItemProperty); }
set { SetValue(PivotItemProperty, value); }
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Loaded += AssociatedObject_Loaded;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.Loaded -= AssociatedObject_Loaded;
}
private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
{
DictionaryIndexes = new Dictionary<PivotItem, int>();
int index = 0;
foreach (PivotItem item in AssociatedObject.Items)
DictionaryIndexes.Add(item, index++);
}
private static void VisiblePropertyChanged(DependencyObject dpObj, DependencyPropertyChangedEventArgs change)
{
var behavior = (PivotItemHideableBehavior)dpObj;
var pivot = behavior.AssociatedObject;
if (!behavior.Visible)
{
if (pivot.Items.Contains(behavior.PivotItem))
pivot.Items.Remove(behavior.PivotItem);
}
else if (!pivot.Items.Contains(behavior.PivotItem))
{
int index = 0;
foreach (var item in behavior.DictionaryIndexes)
{
if (item.Key == behavior.PivotItem)
pivot.Items.Insert(index, behavior.PivotItem);
else if (pivot.Items.Contains(item.Key))
index++;
}
}
}
}
Usage:
<Interactivity:Interaction.Behaviors>
<Behaviors:PivotItemHideableBehavior PivotItem="{x:Bind PivotItemName}" Visible="{Binding IsPivotItemVisible}" />
</Interactivity:Interaction.Behaviors>