In a past few months I've played a lot with the TreeView and now I get to the UI freeze problem. It comes when you have large amount of the items and the data part for those Items are created very quickly but creating TreeViewItems and visualizing those (it must be done on UI thread) takes a time.
Let's take Shell browser and C:\Windows\System32 directory as an example. (I reworked http://www.codeproject.com/Articles/24237/A-Multi-Threaded-WPF-TreeView-Explorer solution for that.) This directory has ~2500 files and folders.
The DataItem and Visual loading are implemented in different threads but as the file and directory info are read quickly it gives no benefit. Application freezes when it creates TreeViewItems and makes those visible.
I've tried:
Set a different DispatcherPriorities for the UI thread when to load items, for example the window was interactive (I was able to move it) with DispatcherPriority.ContextIdle, but then items were loaded really slow..
Create and visualize items in blocks, like 100 items per once, but hat no benefit, the UI thread still were freezing..
My goal is that the application would be interactive while loading those item's!
At the moment I have only one idea how to solve this, to implement my own control which tracks window size, scrollbar position and loads only the items which are visable, but it's not so easy to do that and I'm not sure that at the end performance would be better.. :)
Maybe somebody has idea how to make application interactive while loading bunch of visual items?!
Code:
Complete Solution could be found there: http://www.speedyshare.com/hksN6/ShellBrowser.zip
Program:
public partial class DemoWindow
{
public DemoWindow()
{
InitializeComponent();
this.Loaded += DemoWindow_Loaded;
}
private readonly object _dummyNode = null;
delegate void LoaderDelegate(TreeViewItem tviLoad, string strPath, DEL_GetItems actGetItems, AddSubItemDelegate actAddSubItem);
delegate void AddSubItemDelegate(TreeViewItem tviParent, IEnumerable<ItemToAdd> itemsToAdd);
// Gets an IEnumerable for the items to load, in this sample it's either "GetFolders" or "GetDrives"
// RUNS ON: Background Thread
delegate IEnumerable<ItemToAdd> DEL_GetItems(string strParent);
void DemoWindow_Loaded(object sender, RoutedEventArgs e)
{
var tviRoot = new TreeViewItem();
tviRoot.Header = "My Computer";
tviRoot.Items.Add(_dummyNode);
tviRoot.Expanded += OnRootExpanded;
tviRoot.Collapsed += OnItemCollapsed;
TreeViewItemProps.SetItemImageName(tviRoot, #"Images/Computer.png");
foldersTree.Items.Add(tviRoot);
}
void OnRootExpanded(object sender, RoutedEventArgs e)
{
var treeViewItem = e.OriginalSource as TreeViewItem;
StartItemLoading(treeViewItem, GetDrives, AddItem);
}
void OnItemCollapsed(object sender, RoutedEventArgs e)
{
var treeViewItem = e.OriginalSource as TreeViewItem;
if (treeViewItem != null)
{
treeViewItem.Items.Clear();
treeViewItem.Items.Add(_dummyNode);
}
}
void OnFolderExpanded(object sender, RoutedEventArgs e)
{
var tviSender = e.OriginalSource as TreeViewItem;
e.Handled = true;
StartItemLoading(tviSender, GetFilesAndFolders, AddItem);
}
void StartItemLoading(TreeViewItem tviSender, DEL_GetItems actGetItems, AddSubItemDelegate actAddSubItem)
{
tviSender.Items.Clear();
LoaderDelegate actLoad = LoadSubItems;
actLoad.BeginInvoke(tviSender, tviSender.Tag as string, actGetItems, actAddSubItem, ProcessAsyncCallback, actLoad);
}
void LoadSubItems(TreeViewItem tviParent, string strPath, DEL_GetItems actGetItems, AddSubItemDelegate actAddSubItem)
{
var itemsList = actGetItems(strPath).ToList();
Dispatcher.BeginInvoke(DispatcherPriority.Normal, actAddSubItem, tviParent, itemsList);
}
// Runs on Background thread.
IEnumerable<ItemToAdd> GetFilesAndFolders(string strParent)
{
var list = Directory.GetDirectories(strParent).Select(itemName => new ItemToAdd() {Path = itemName, TypeOfTheItem = ItemType.Directory}).ToList();
list.AddRange(Directory.GetFiles(strParent).Select(itemName => new ItemToAdd() {Path = itemName, TypeOfTheItem = ItemType.File}));
return list;
}
// Runs on Background thread.
IEnumerable<ItemToAdd> GetDrives(string strParent)
{
return (Directory.GetLogicalDrives().Select(x => new ItemToAdd(){Path = x, TypeOfTheItem = ItemType.DiscDrive}));
}
void AddItem(TreeViewItem tviParent, IEnumerable<ItemToAdd> itemsToAdd)
{
string imgPath = "";
foreach (ItemToAdd itemToAdd in itemsToAdd)
{
switch (itemToAdd.TypeOfTheItem)
{
case ItemType.File:
imgPath = #"Images/File.png";
break;
case ItemType.Directory:
imgPath = #"Images/Folder.png";
break;
case ItemType.DiscDrive:
imgPath = #"Images/DiskDrive.png";
break;
}
if (itemToAdd.TypeOfTheItem == ItemType.Directory || itemToAdd.TypeOfTheItem == ItemType.File)
IntAddItem(tviParent, System.IO.Path.GetFileName(itemToAdd.Path), itemToAdd.Path, imgPath);
else
IntAddItem(tviParent, itemToAdd.Path, itemToAdd.Path, imgPath);
}
}
private void IntAddItem(TreeViewItem tviParent, string strName, string strTag, string strImageName)
{
var tviSubItem = new TreeViewItem();
tviSubItem.Header = strName;
tviSubItem.Tag = strTag;
tviSubItem.Items.Add(_dummyNode);
tviSubItem.Expanded += OnFolderExpanded;
tviSubItem.Collapsed += OnItemCollapsed;
TreeViewItemProps.SetItemImageName(tviSubItem, strImageName);
tviParent.Items.Add(tviSubItem);
}
private void ProcessAsyncCallback(IAsyncResult iAR)
{
// Call end invoke on UI thread to process any exceptions, etc.
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, (Action)(() => ProcessEndInvoke(iAR)));
}
private void ProcessEndInvoke(IAsyncResult iAR)
{
try
{
var actInvoked = (LoaderDelegate)iAR.AsyncState;
actInvoked.EndInvoke(iAR);
}
catch (Exception ex)
{
// Probably should check for useful inner exceptions
MessageBox.Show(string.Format("Error in ProcessEndInvoke\r\nException: {0}", ex.Message));
}
}
private struct ItemToAdd
{
public string Path;
public ItemType TypeOfTheItem;
}
private enum ItemType
{
File,
Directory,
DiscDrive
}
}
public static class TreeViewItemProps
{
public static string GetItemImageName(DependencyObject obj)
{
return (string)obj.GetValue(ItemImageNameProperty);
}
public static void SetItemImageName(DependencyObject obj, string value)
{
obj.SetValue(ItemImageNameProperty, value);
}
public static readonly DependencyProperty ItemImageNameProperty;
static TreeViewItemProps()
{
ItemImageNameProperty = DependencyProperty.RegisterAttached("ItemImageName", typeof(string), typeof(TreeViewItemProps), new UIPropertyMetadata(string.Empty));
}
}
Xaml:
<Window x:Class="ThreadedWpfExplorer.DemoWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ThreadedWpfExplorer"
Title="Threaded WPF Explorer" Height="840" Width="350" Icon="/ThreadedWpfExplorer;component/Images/Computer.png">
<Grid>
<TreeView x:Name="foldersTree">
<TreeView.Resources>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate DataType="ContentPresenter">
<Grid>
<StackPanel Name="spImg" Orientation="Horizontal">
<Image Name="img"
Source="{Binding
RelativeSource={RelativeSource
Mode=FindAncestor,
AncestorType={x:Type TreeViewItem}},
Path=(local:TreeViewItemProps.ItemImageName)}"
Width="20" Height="20" Stretch="Fill" VerticalAlignment="Center" />
<TextBlock Text="{Binding}" Margin="5,0" VerticalAlignment="Center" />
</StackPanel>
</Grid>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</TreeView.Resources>
</TreeView>
</Grid>
</Window>
Alternative Loading items in blocks:
private const int rangeToAdd = 100;
void LoadSubItems(TreeViewItem tviParent, string strPath, DEL_GetItems actGetItems, AddSubItemDelegate actAddSubItem)
{
var itemsList = actGetItems(strPath).ToList();
int index;
for (index = 0; (index + rangeToAdd) <= itemsList.Count && rangeToAdd <= itemsList.Count; index = index + rangeToAdd)
{
Dispatcher.BeginInvoke(DispatcherPriority.Normal, actAddSubItem, tviParent, itemsList.GetRange(index, rangeToAdd));
}
if (itemsList.Count < (index + rangeToAdd) || rangeToAdd > itemsList.Count)
{
var itemsLeftToAdd = itemsList.Count % rangeToAdd;
Dispatcher.BeginInvoke(DispatcherPriority.Normal, actAddSubItem, tviParent, itemsList.GetRange((rangeToAdd > itemsList.Count) ? index : index - rangeToAdd, itemsLeftToAdd));
}
}
What you're looking for is known as UI Virtualization and is supported by a number of different WPF controls. Regarding the TreeView in particular, see this article for details on how to turn on virtualization.
One major caveat is that in order to benefit from this feature, you need to use the ItemsSource property and provide items from a collection rather than adding items directly from your code. This is a good idea to do anyway, but it may require some restructuring to get it functional with your existing code.
Why not just create your observable collection and bind to it from xaml?
Check out the MvvM design pattern and you just create a class, and point the xaml at it, in there, from the initialisation, create your list, and then tell the treeview to bind to that list, displaying properties of the each item in your list.
I know this is a little scant on info, but to do MvvM is really easy and just look through stackoverflow and you'll see examples.
You really don't need to call begininvoke on every item - and that's just not from an mvvm point of view - just bind to a list.
You can use indexed 'levels' to your objects too.
Another helpful technique is this regard, is Data Virtualization. There is a good article and sample project on CodeProject, that talks about Data Virtualization in WPF.
Related
In my View I have a Search Box for filtering and a tree that should be automatically expanded on specific conditions and when the search string is changed.
So the user should be able to see all nodes that are found.
I'm using a TreeListView which is a simple TreeView with columns, it also behaves like one.
Also I'm using a ControlTemplate.
Because the control template is ignoring any ItemsSource set in the original TreeView I'm using a hack:
ControlTemplate:
<ControlTemplate x:Key="TreeControlTemplate">
<Border>
<treeList:TreeListView ItemsSource="{Binding DataContext, RelativeSource={RelativeSource AncestorType={x:Type treeList:TreeListView}}}"
[...]
TreeView:
<treeList:TreeListView Template="{StaticResource TreeControlTemplate}"
DataContext="{Binding RootItem.FilteredChildren}" />
This works so far so good.
When the Filter is changing there is a NotifyOfPropertyChange(() => FilteredChildren);
To expand the tree I'm using this code in code-behind:
private ViewModel _viewModel;
public View()
{
InitializeComponent();
DataContextChanged += OnDataContextChanged;
Tree.DataContextChanged += Tree_DataContextChanged;
}
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (DataContext is ViewModel viewModel)
{
DataContextChanged -= OnDataContextChanged;
_viewModel = viewModel;
}
}
private async void Tree_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
await Task.Delay(500);
if (_viewModel.ShouldExpandTreesAfterUpdate())
ExpandTree(Tree);
}
private void ExpandTree(DependencyObject parent)
{
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
if (child is TreeListViewItem treeListViewItem)
treeListViewItem.ExpandSubtree();
else
ExpandTree(child);
}
}
This works, but only when I use the Task.Delay
There are a few downsides I am not able to overcome tho:
The Task.Delay seems to be required otherwise the UI is probably not ready. How can I do without?
It would be nice to do this without accessing the ViewModel from View code-behind (optional)
Since I'm binding my ItemsSource the Items property seems to be empty and I'm basicially crawling the entire visual tree just to find the actual tree items. (optional)
How can I do without?
You need to wait until the tree has been refreshed based on the new DataContext one way or another. Using the Dispatcher to execute a delegate at a specified priority is one option, e.g.:
private async void Tree_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
_ = Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Background, () =>
{
if (_viewModel.ShouldExpandTreesAfterUpdate())
ExpandTree(Tree);
});
}
I have a test app where i'm trying to insert an editable paragraph so user can write info there (maybe it can be realized just with Run, i took paragraph just for example, if you know how to add Run into Run, that will be great). I DON'T WANT to use richtextbox for it for two main reasons:
User can't edit any other parts of document
Flowdocument has pagination
For what i've done now, i have this: textbox and flowdocument with one paragraph (aaaaa bbb cccc) created by xaml and one created by code
My editable paragraph going to the end of document. What i want is to put it instead of "bbb" for examle. So it must somehow find "bbb" from all document, replace it, and put in that place my paragraph
I've tried to:
Run through all blocks, find text that i need and remove it from paragraph, but no use because i can't replace string with paragraph or with run
Find index of text i want but i still can't do nothing with it because i need a TextPointer
Convert int to TextPointer but documentation said i'm going to a wrong and unsave direction
Find cursor controller for FlowDocument and set it to index i need but it still needs a TextPointer
So i really need help because i can't see no other options
Here is my xaml
<Grid x:Name="grid">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<FlowDocumentReader Grid.Row="1">
<FlowDocument x:Name="DocumentReader"/>
</FlowDocumentReader>
</Grid>
And here is my xaml.cs without any bad code with my attempts to set paragrahp inside a paragraph - just textbox and editable paragraph
Dictionary<string, Paragraph> paragraphs = new Dictionary<string, Paragraph>();
private string text = $"{{\\rtf1\\ansi\\ansicpg1252\\uc1\\htmautsp\\deff2{{\\fonttbl{{\\f0\\fcharset0 Times New Roman;}}{{\\f2\\fcharset0 Palatino Linotype;}}}}{{\\colortbl\\red0\\green0\\blue0;\\red255\\green255\\blue255;}}\\loch\\hich\\dbch\\pard\\plain\\ltrpar\\itap0{{\\lang1033\\fs21\\f2\\cf0 \\cf0\\ql{{\\f2 {{\\ltrch aaaaa bbb ccc}}\\li0\\ri0\\sa0\\sb0\\fi0\\ql\\par}}\r\n}}\r\n}}";
public MainWindow()
{
InitializeComponent();
//this is how data loads in flowdocument in my actual programm
TextRange textRange = new TextRange(DocumentReader.ContentStart, DocumentReader.ContentEnd);
using (MemoryStream stream = new MemoryStream(ASCIIEncoding.Default.GetBytes(text)))
{
textRange.Load(stream, DataFormats.Rtf);
}
//this is what i was testing
var parag = new Paragraph { Name = "paragName" };
parag.Inlines.Add(new Run("as"));
paragraphs.Add("paragName", parag);
DocumentReader.Blocks.Add(parag);
var txt = new TextBox{Tag = "paragName" };
txt.TextChanged += (sender, args) =>
{
paragraphs.First(x => (string)x.Key == txt.Tag).Value.Inlines.Clear();
paragraphs.First(x => (string)x.Key == txt.Tag).Value.Inlines.Add(new Run((sender as TextBox).Text));
};
grid.Children.Add(txt);
}
It is super raw, i was just testing it, but i can't resolve how to do it, please help
A quite simple solution would be to use a TextBlock and then inline a TextBox at the position you like to edit.
The following example lets EditableTextBlock extend TextBlock to extend the TextBlock behavior.
Setting a EditableTextBlock.EditableTextRange property defines the position and range in the text which should be made editable. The complete displayed text can be obtained by accessing the inherited EditableTextBlock.Text property as usual.
Setting the EditableTextBlock.EditableTextRange property will trigger a TextBox to appear at the specified position. The edited value is then committed by pressing the Enter key or by clicking the Button next to the TextBox.
The TextBox will then disappear and the edited text will become read-only again.
To simplify the content handling, the EditableTextBlock maintains a single Run to display the content.
The implementation is very simple and should serve you as a useful starting point.
TextRange.cs
public readonly struct TextRange : IEquatable<TextRange>
{
public TextRange(int index, int length)
{
// TODO::Throw ArgumentException if values are out of range (e.g. < 0)
this.Index = index;
this.Length = length;
}
public bool Equals(TextRange other) => this.Index.Equals(other.Index) && this.Length.Equals(other.Length);
public int Index { get; }
public int Length { get; }
}
EditableTextBlock.cs
public class EditableTextBlock : TextBlock
{
public TextRange EditableTextRange
{
get => (TextRange)GetValue(EditableTextRangeProperty);
set => SetValue(EditableTextRangeProperty, value);
}
public static readonly DependencyProperty EditableTextRangeProperty = DependencyProperty.Register(
"EditableTextRange",
typeof(TextRange),
typeof(EditableTextBlock),
new PropertyMetadata(default(TextRange), OnTextRangeChanged));
public static void SetText(UIElement attachedElement, string value)
=> attachedElement.SetValue(TextProperty, value);
public static string GetText(UIElement attachedElement)
=> attachedElement.GetValue(TextProperty) as string;
public static readonly DependencyProperty TextProperty = DependencyProperty.RegisterAttached(
"Text",
typeof(string),
typeof(EditableTextBlock),
new FrameworkPropertyMetadata(default(string), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public static RoutedUICommand CommitChangesCommand { get; }
static EditableTextBlock()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(EditableTextBlock), new FrameworkPropertyMetadata(typeof(EditableTextBlock)));
CommitChangesCommand = new RoutedUICommand(
"Commit edit changes",
nameof(CommitChangesCommand),
typeof(EditableTextBlock),
new InputGestureCollection()
{
new KeyGesture(Key.Enter)
});
}
public EditableTextBlock()
{
var editableElementContentTemplate = Application.Current.Resources["EditableElementTemplate"] as DataTemplate;
if (editableElementContentTemplate == null)
{
throw new InvalidOperationException("Define a DataTemplate named "EditableElementTemplate" in App.xaml");
}
var editableContent = new ContentPresenter() { ContentTemplate = editableElementContentTemplate };
this.EditableElement = new InlineUIContainer(editableContent);
this.CommandBindings.Add(new CommandBinding(CommitChangesCommand, ExecuteCommitChangesCommand));
}
private static void OnTextRangeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
=> (d as EditableTextBlock).OnTextRangeChanged((TextRange)e.OldValue, (TextRange)e.NewValue);
private void ExecuteCommitChangesCommand(object sender, ExecutedRoutedEventArgs e)
{
var documentTextBuilder = new StringBuilder();
foreach (Inline documentElement in this.Inlines)
{
documentTextBuilder.Append(documentElement is Run run ? run.Text : GetText((documentElement as InlineUIContainer).Child));
}
var readOnlyDocument = new Run(documentTextBuilder.ToString());
this.Inlines.Clear();
this.Inlines.Add(readOnlyDocument);
}
protected virtual void OnTextRangeChanged(TextRange oldTextRange, TextRange newTextRange)
{
Inline documentContent = this.Inlines.FirstInline;
if (documentContent is Run run && newTextRange.Index < run.Text.Length)
{
string newPreceedingReadOnlyRangeText = run.Text.Substring(0, newTextRange.Index);
var newReadOnlyElement = new Run(newPreceedingReadOnlyRangeText);
this.Inlines.InsertBefore(documentContent, newReadOnlyElement);
string newEditableRangeText = run.Text.Substring(newTextRange.Index, newTextRange.Length);
SetText(this.EditableElement.Child, newEditableRangeText);
this.Inlines.InsertAfter(documentContent, this.EditableElement);
this.Inlines.Remove(documentContent);
string remainingReadOnlyRangeText = run.Text.Substring(newTextRange.Index + newTextRange.Length);
var remainingReadOnlyElement = new Run(remainingReadOnlyRangeText);
this.Inlines.InsertAfter(this.EditableElement, remainingReadOnlyElement);
}
else // Append
{
string newEditableRangeText = String.Empty;
SetText(this.EditableElement.Child, newEditableRangeText);
this.Inlines.Add(this.EditableElement);
}
}
private InlineUIContainer EditableElement { get; }
}
App.xaml
<Application xmlns:system="clr-namespace:System;assembly=netstandard">
<Application.Resources>
<DataTemplate x:Key="EditableElementTemplate"
DataType="{x:Type system:String}">
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding RelativeSource={RelativeSource AncestorType=ContentPresenter}, Path=(local:EditableTextBlock.Text), UpdateSourceTrigger=PropertyChanged}" />
<Button Command="{x:Static local:EditableTextBlock.CommitChangesCommand}"
Content="Ok" />
</StackPanel>
</DataTemplate>
</Application.Resources>
</Application>
Usage example
MainWindow.xaml
<Window>
<local:EditableTextBlock x:Name="Document" />
</Window>
MainWindow.xaml.cs
partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.Loaded #= OnLoaded;
}
private void OnLoaded(object sender, EventArgs e)
{
var documentText = "This is some random text.";
this.Document.Text = documentText;
int editableTextIndex = this.Document.Text.IndexOf("random");
int editableTextLength = "random".Length;
this.Document.EditableTextRange = new TextRange(editableTextIndex, editableTextLength);
}
}
I am creating a WPF application to act as a front end for a video games library and I'm attempting to mimic the Netflix UI. One of the features in this application is to cycle through images of games to select which game you want to play.
The desired behavior is different than the behavior when arrowing through the items in a ListBox: when you arrow through items in a ListBox, your selection moves up and down. The behavior I'm looking to implement is that as you arrow through the items, the selected item is always at the first position and the items are cycling across the selector. The term for this would be a carousel where the selected item is at index 0.
I've implemented this poorly and to give some context, here's a picture of how my interface currently looks:
My current implementation
To achieve this, I believe what I should do is extend the StackPanel class or maybe implement my own Panel. But details on custom panels are a bit complicated and hard to come by. I want to show what I've done to this point to get this working but I'm very unhappy with these implementations and I'd like to get some advice on what direction I should go for a proper implementation.
Here are some details on what I've tried.
The screenshot above is a result of a GameList class that I created which implements INotifyPropertyChanged and includes properties for 15 different games.
private GameMatch game0;
public GameMatch Game0
{
get { return game0; }
set
{
if (game0 != value)
{
game0 = value;
PropertyChanged(this, new PropertyChangedEventArgs("Game0"));
}
}
}
private GameMatch game1;
public GameMatch Game1
{
get { return game1; }
set
{
if (game1 != value)
{
game1 = value;
PropertyChanged(this, new PropertyChangedEventArgs("Game1"));
}
}
}
// identical code for games 2-10
private GameMatch game11;
public GameMatch Game11
{
get { return game11; }
set
{
if (game11 != value)
{
game11 = value;
PropertyChanged(this, new PropertyChangedEventArgs("Game11"));
}
}
}
private GameMatch game12;
public GameMatch Game12
{
get { return game12; }
set
{
if (game12 != value)
{
game12 = value;
PropertyChanged(this, new PropertyChangedEventArgs("Game12"));
}
}
}
I've laid the images out in my XAML and added enough so that they will run off the edge of the screen:
<StackPanel Name="GameImages" Orientation="Horizontal">
<Border BorderThickness="2" BorderBrush="AntiqueWhite">
<Image Name="Image_Game1" Source="{Binding CurrentGameList.Game1.FrontImage}"/>
</Border>
<Image Source="{Binding CurrentGameList.Game2.FrontImage}"/>
<!-- identical images for games 3-10 -->
<Image Source="{Binding CurrentGameList.Game11.FrontImage}" />
<Image Source="{Binding CurrentGameList.Game12.FrontImage}" />
</StackPanel>
I implemented a ListCycle class which can take any arbitrary list and a count of items that you want to cycle. In case it helps, here's the code for the ListCycle. It takes care of cycling the lists by tracking the index of items in list that should be displayed on screen in a given position.
public class ListCycle<T>
{
// list of games or whatever you want
public List<T> GenericList { get; set; }
// indexes currently available to display
// will cycle around the size of the generic list
public int[] indices;
public ListCycle(List<T> genericList, int activeCycleCount)
{
GenericList = genericList;
indices = new int[activeCycleCount];
InitializeIndices();
}
private void InitializeIndices()
{
if (GenericList != null)
{
int lastIndex = -1;
for (int i = 0; i < indices.Length; i++)
{
indices[i] = GetNextIndex(lastIndex);
lastIndex = indices[i];
}
}
}
private int GetNextIndex(int currentIndex)
{
currentIndex += 1;
if (currentIndex == GenericList.Count)
{
currentIndex = 0;
}
return currentIndex;
}
private int GetPreviousIndex(int currentIndex)
{
currentIndex -= 1;
if (currentIndex == -1)
{
currentIndex = GenericList.Count - 1;
}
return currentIndex;
}
public int GetIndexValue(int index)
{
return indices[index];
}
public T GetItem(int index)
{
return GenericList[indices[index]];
}
public void CycleForward()
{
for (int i = 0; i < indices.Length; i++)
{
if (i + 1 < indices.Length - 1)
{
indices[i] = indices[i + 1];
}
else
{
indices[i] = GetNextIndex(indices[i]);
}
}
}
public void CycleBackward()
{
for (int i = indices.Length - 1; i >= 0; i--)
{
if(i - 1 >= 0)
{
indices[i] = indices[i - 1];
}
else
{
indices[i] = GetPreviousIndex(indices[i]);
}
}
}
}
So when you press right, I cycle forward and reset the game images. When you press left, I cycle backward and reset the game images. The RefreshGames method takes care of updating all of those game properties in my game list.
private void RefreshGames()
{
Game0 = gameCycle.GetItem(0);
Game1 = gameCycle.GetItem(1);
Game2 = gameCycle.GetItem(2);
Game3 = gameCycle.GetItem(3);
Game4 = gameCycle.GetItem(4);
Game5 = gameCycle.GetItem(5);
Game6 = gameCycle.GetItem(6);
Game7 = gameCycle.GetItem(7);
Game8 = gameCycle.GetItem(8);
Game9 = gameCycle.GetItem(9);
Game10 = gameCycle.GetItem(10);
Game11 = gameCycle.GetItem(11);
Game12 = gameCycle.GetItem(12);
}
This approach works but it doesn't work well. It's not dynamic, it doesn't scale well and it doesn't perform all that well. Arrowing through images one at a time performs just fine but trying to quickly move through them, is a bit slow and feels clunky. It's not a very good user experience.
I tried a second approach, using a listbox bound the to my list of games. And to cycle the games to the left, I would remove the first item from my list of games and insert it at the end of the list. To go to the right, I would remove the item at the end of the list and insert it at index 0. This also worked but it didn't perform very well either.
So I'm looking for suggestions on a better way to implement this that would give better performance (smoother scrolling) and be more dynamic (i.e. this approach may not work well on an ultrawide monitor - 12 games may not be enough depending on the widths of the images). I'm not looking for anyone to solve it for me but point me in the right direction as I'm very new to WPF.
My feeling is that I should be extending the stack panel class and changing the way you cycle through the items or maybe creating my own panel. Can anyone confirm if this is the best approach and if so point me to some good resources to help me understand how to create a custom panel that changes the way navigation is done? I've been reading articles on creating custom panels to try to get my head around that process.
Being new to WPF, I want to make sure I'm not going down a rabbit hole or trying to reinvent a wheel that already exists. So the question is whether a custom panel is the right approach to solving this problem?
I believe what I should do is extend the StackPanel class
WPF encourages composition of existing Controls over inheritance; in your case inheriting the StackPanel looks too complicated for your purpose when you could achieve the same with your second approach:
I would remove the first item from my list of games and insert it at the end of the list
This indeed looks more like idiomatic WPF, especially if you try to follow the MVVM design pattern.
or maybe creating my own panel
This is not an easy step especially if you're new to WPF but that would be very interesting for you. That could be a way to go, especially if you internally rely on a StackPanel (composition) instead of inheriting from it.
Example implementation with an ItemsControl
I will use an ItemsControl which can display a collection of data for you (in your case, you have some GameMatch).
First define the data behind the interface, ie a collection of GameMatch. Let's give each GameMatch a name and a variable IsSelected which tells if the game is selected (ie in first position). I'm not showing the INotifyPropertyChanged implementation but it should be there for both properties.
public class GameMatch : INotifyPropertyChanged {
public string Name { get => ...; set => ...; }
public bool IsSelected { get => ...; set => ...; }
}
Your carousel interface is interested in a collection of GameMatch, so let's create an object to model this.
Our graphical interface is gonna bind to the Items property to display the collection of games.
It is also gonna bind to the two commands that are implemented such as to shift the list to the left or to the right. You can use the RelayCommand to create commands. In a nutshell, a Command is simply an action that gets executed and that you can easily refer to from your interface.
public class GameCollection {
// Moves selection to next game
public ICommand SelectNextCommand { get; }
// Moves selection to previous game
public ICommand SelectPreviousCommand { get; }
public ObservableCollection<GameMatch> Items { get; } = new ObservableCollection<GameMatch> {
new GameMatch() { Name = "Game1" },
new GameMatch() { Name = "Game2" },
new GameMatch() { Name = "Game3" },
new GameMatch() { Name = "Game4" },
new GameMatch() { Name = "Game5" },
};
public GameCollection() {
SelectNextCommand = new RelayCommand(() => ShiftLeft());
SelectPreviousCommand = new RelayCommand(() => ShiftRight());
SelectFirstItem();
}
private void SelectFirstItem() {
foreach (var item in Items) {
item.IsSelected = item == Items[0];
}
}
public void ShiftLeft() {
// Moves the first game to the end
var first = Items[0];
Items.RemoveAt(0);
Items.Add(first);
SelectFirstItem();
}
private void ShiftRight() {
// Moves the last game to the beginning
var last = Items[Items.Count - 1];
Items.RemoveAt(Items.Count - 1);
Items.Insert(0, last);
SelectFirstItem();
}
}
The key here is the ObservableCollection class which will tell the view whenever it changes (for example, everytime we move items around inside it) so the view will update to reflect this.
Then, the view (your XAML) should specify how to display the collection of games. We're gonna use an ItemsControl laying out items horizontally:
<StackPanel>
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Margin="10" Background="Beige" BorderBrush="Black" Width="150" Height="50">
<Border.Style>
<Style TargetType="Border">
<Setter Property="BorderThickness" Value="1" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="true">
<Setter Property="BorderThickness" Value="5" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock Text="{Binding Name}"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Content="Previous" Command="{Binding SelectPreviousCommand}"/>
<Button Content="Next" Command="{Binding SelectNextCommand}"/>
</StackPanel>
</StackPanel>
Notice the ItemsControl ItemsSource="{Binding Items}" which tells the ItemsControl to display all the objects in the Items property. The ItemsControl.ItemsPanel part tells to lay them out in an horizontal StackPanel. The ItemsControl.ItemTemplate part explains how each game should be displayed, and the DataTrigger within tells WPF to increase the border thickness for the selected item. Finally, the StackPanel at the bottom displays two Button which call SelectPreviousCommand and SelectLeftCommand in our GameCollection.
Finally, you should set the DataContext of the whole thing to a new GameCollection:
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
DataContext = new GameCollection();
}
}
From there you can customize the UI as you'd like.
Animations and smooth scrolling
That is a whole other topic but you could for example trigger a translation animation of all your items when clicking one of the buttons.
I'll try and point you in the right direction. If you haven't already checked it out, I would try to make your application follow the MVVM pattern. In your case, the ViewModel would have an ObservableCollection of "Games". You would then bind your ItemsControl's source to that collection.
As far as getting the carousel to work, I think the path you will want to go down is creating a custom ItemsControl or ListBox. You can override the styling and create some custom behavior to get the carousel to work how you would like it to.
I can probably help out more if you have a more specific question.
I opened the question here but we cannot come to the solution for my problem. I decided to create new question as we came to some assumptions and the former question does not refer to the real problem(we thought it is the problem with binding but as you will read on it is not).
In few words I have a ListView with data from list called jointList.
The list is doing well and it has all the data necessary. (I checked it)
On each row of the ListView I put a ToggleSwitch(in xaml) and then I try to do something with each of the switches.
Each switch should correspond to the data from the same row.
I created Toggled event that should apply to all toggleSwitches like this:
private void ToggleSwitch_Toggled(object sender, RoutedEventArgs e)
{
foreach (var product in jointList)
{
if (product.IsOn == true)
{
ToggleTest.Text = product.ProductId.ToString(); // this is for testing only, later I would do something with the data retrieved
ToggleTest.Visibility = Visibility.Visible;
}
else
{
ToggleTest.Visibility = Visibility.Collapsed;
}
}
}
But this is making only one toggleSwitch work. It's the switch that corresponds to the last added product to the list ( I am guessing that it is refering to the last Id). The other switches return nothing as if the method was not iterating through the list correctly or as if there was only one switch hooked up.
So, is it possible to get all switches up and running by using just one Toggled event as I attempt to do?
Here's a sample which shows one way.
In this example we have the following Product view model:
public class Product : INotifyPropertyChanged
{
private string _name;
public string Name
{
get => _name;
set
{
if (value == _name) return;
_name = value;
OnPropertyChanged();
}
}
So just a single Name-property.
Then we have MainPage where we create a collection of products:
private void FrameworkElement_OnLoaded(object sender, RoutedEventArgs e)
{
var items = new ObservableCollection<Product>();
for (int i = 0; i < 9; i++)
{
items.Add(new Product($"item {i}"));
}
this.Items.ItemsSource = items;
}
And the XAML which creates the view:
<ListView Loaded="FrameworkElement_OnLoaded" x:Name="Items">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="RowContent" Text="{Binding Name}"/>
<ToggleSwitch x:Name="Toggle" Grid.Column="1" Toggled="Toggle_OnToggled"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
The result:
Now we want to change the text when user toggles the switch. This is done in Toggle_OnToggled-event handler:
private void Toggle_OnToggled(object sender, RoutedEventArgs e)
{
var toggle = (ToggleSwitch) sender;
var dataContext = ((Grid)toggle.Parent).DataContext;
var dataItem = (Product) dataContext;
dataItem.Name = $"Toggled {toggle.IsOn}";
}
So after a few toggles:
Mikael Koskinen has delivered the answer to my problem.
Most of my code was correct and identical to his solution, apart from the last bit that is OnToggled event handler.
Here is the working andd correct handler:
private void Toggle_OnToggled(object sender, RoutedEventArgs e)
{
var toggle = (ToggleSwitch)sender;
var dataContext = ((Grid)toggle.Parent).DataContext;
var dataItem = (ScheduleList)dataContext;
ToggleTest.Text = dataItem.ProductId;
}
My previous version of handler didn't include the important bit, that is dataContext and dataItem.
It works like a charm now.
I am using CollectionViewSource to filter the records displayed in a ListBox. The xaml follows.
<Window x:Class="WPFStarter.ListBoxItemsFilter.ListBoxFilterUsingCollectionViewSource"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="userControl"
Title="ListBoxFilterUsingCollectionViewSource" Height="300" Width="300">
<Window.Resources>
<CollectionViewSource Source="{Binding ElementName=userControl, Path=DataContext.Items}"
x:Key="cvs" Filter="CollectionViewSource_Filter"/>
</Window.Resources>
<StackPanel Orientation="Vertical">
<TextBox x:Name="txtSearch" TextChanged="txtSearch_TextChanged"/>
<TextBlock x:Name="txtSummary" Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Bottom" FontSize="8"></TextBlock>
<ListBox ItemsSource="{Binding Source={StaticResource cvs}}" DisplayMemberPath="First"/>
</StackPanel>
</Window>
And here is my code-behing ( please don;t mind this code-behind, in the real application i am using the best of MVVM for this scenario).
public partial class ListBoxFilterUsingCollectionViewSource : Window
{
private string _text="";
private readonly CollectionViewSource _viewSource;
public ListBoxFilterUsingCollectionViewSource()
{
InitializeComponent();
_viewSource = this.FindResource("cvs") as CollectionViewSource;
}
private void CollectionViewSource_Filter(object sender, FilterEventArgs e)
{
var character = e.Item as Character;
e.Accepted = character != null && character.First.ToLower().Contains(_text.ToLower());
}
private void txtSearch_TextChanged(object sender, TextChangedEventArgs e)
{
_text = txtSearch.Text;
_viewSource.View.Refresh();
SetSummary();
}
private void SetSummary()
{
var initialCount = 10; //HELP????
var filteredCount = 10; //HELP????
txtSummary.Text = String.Format("{0} of {1}", filteredCount, initialCount);
}
}
QUESTION:
I Need help in writing the "SetSummary" method, wherein i can get the "initialCount" and the "filteredCount" from CollectionViewSource object.
Thanks for your interest.
You could also do _viewSource.View.Cast<object>().Count() for the filtered list and _viewSource.View.SourceCollection.Cast<object>().Count() for the original.
I think the better solution is, as usual, Linq!
_viewSource.View.Cast<[your_type]>().Count();
...or...
_viewSource.View.Cast<object>().Count();
...if you don't know the items' type at runtime!
The source collection and collectionview both implements IEnumerable so you can always iterate over them and count how many are in them. But I would only recommend doing this if you have no access to the actual collection you used as source.
private void SetSummary()
{
int initialCount = 0;
foreach(var item in _viewSource.View.SourceCollection)
{
initialCount++;
}
int filteredCount = 0;
foreach (var item in _viewSource.View)
{
filteredCount++;
}
}
If you're doing MVVM, you could have your VM create a collection view rather than one being created on your behalf by the CollectionViewSource. Then, you have control over what type of CVS is created, so you can create a ListCollectionViewSource, which has a Count property. It really depends on the properties of the data you're filtering.
var count = DataGrid.ItemsSource.OfType<object>().Count();
public static int Count(this ICollectionView view)
{
var index = 0;
foreach (var unused in view)
{
index++;
}
return index;
}