Binding Collection of Collection to one Canvas Element - c#

My last question was marked as duplicate so I try to show what is the difference.
Is there any way to bind Layers and don't create a Canvas element for each one?
If not, then it almost works but:
Multiple canvases overlap. If I set Background property, canvases below will be not visible. Even if Background is set to Transparent, then the Mouse Events are taken only by the Canvas on top.
If I set ClipToBounds property to True (and don't set Width&Height) Markers are not visible. The Width and Height is not the same as main canvas. How to bind these Properties to Main Canvas Width and Height. I know that every Layer will have the same dimensions, so I don't think it would be good to store duplicate information in every Layer.
EDIT:
Sorry for misunderstanding. I try to be more clarify:
Problems (questions) I want to solve are:
Is there any way to bind Layers and don't create a Canvas element for each one?
Now I have mainCanvas + multiple innerCanvases. Could it be just mainCanvas? Does it have any influence on rendering performance?
How to set Width and Height of inner Canvases, so they will have the same dimensions as main Canvas, without binding?
mainCanvas automatically fills all the space, but innerCanvases don't. ClipToBounds=True must be set on innerCanvases.
Tried HorizontalAligment=Stretch but it is not working.
The overlapping: Okay, I think I missed something.
If I don't set Background at all it works fine, as it should. It was just funny for me that not setting Background doesn't work the same as Background=Transparent.**
Sorry for my English.
EDIT: Thanks for your answer
I think it will be better if I don't complicate my code, at least for now. I found out how to bind to ActualWidth as you said:
<Canvas Width="{Binding ElementName=mainCanvas, Path=ActualWidth}"/>
or set ClipToBounds=True on mainCanvas, not the inner ones. I just wanted markers that have Positions X, Y outside mainCanvas dimensions to not be visible. Thats why I needed to set Width, Height of innerCanvases.
Everything is working now, marked as answer.
Here is my code:
ViewModel.cs
public class ViewModel
{
public ObservableCollection<LayerClass> Layers
{ get; set; }
public ViewModel()
{
Layers = new ObservableCollection<LayerClass>();
for (int j = 0; j < 10; j++)
{
var Layer = new LayerClass();
for (int i = 0; i < 10; i++)
{
Layer.Markers.Add(new MarkerClass(i * 20, 10 * j));
}
Layers.Add(Layer);
}
}
}
LayerClass.cs
public class LayerClass
{
public ObservableCollection<MarkerClass> Markers
{ get; set; }
public LayerClass()
{
Markers = new ObservableCollection<MarkerClass>();
}
}
MarkerClass.cs
public class MarkerClass
{
public int X
{ get; set; }
public int Y
{ get; set; }
public MarkerClass(int x, int y)
{
X = x;
Y = y;
}
}
MainWindow.xaml.cs
public partial class MainWindow : Window
{
private ViewModel _viewModel = new ViewModel();
public MainWindow()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
this.DataContext = _viewModel;
}
private void Ellipse_MouseEnter(object sender, MouseEventArgs e)
{
Ellipse s = (Ellipse)sender;
s.Fill = Brushes.Green;
}
private void Ellipse_MouseLeave(object sender, MouseEventArgs e)
{
Ellipse s = (Ellipse)sender;
s.Fill = Brushes.Black;
}
}
MainWindow.xaml
<Window x:Class="TestSO33742236WpfNestedCollection.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:p="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:TestSO33742236WpfNestedCollection"
Loaded="Window_Loaded"
Title="MainWindow" Height="350" Width="525">
<ItemsControl ItemsSource="{Binding Path=Layers}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="LightBlue">
</Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type c:LayerClass}">
<ItemsControl ItemsSource="{Binding Path=Markers}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas x:Name="myCanvas"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Width="20" Height="20" Fill="Black" MouseEnter="Ellipse_MouseEnter" MouseLeave="Ellipse_MouseLeave"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<p:Style> <!-- Explicit namespace to workaround StackOverflow XML formatting bug -->
<Setter Property="Canvas.Left" Value="{Binding Path=X}"></Setter>
<Setter Property="Canvas.Top" Value="{Binding Path=Y}"></Setter>
</p:Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>

Is there any way to bind Layers and don't create a Canvas element for each one?
Now I have mainCanvas + multiple innerCanvases. Could it be just mainCanvas? Does it have any influence on rendering performance?
Certainly it is possible to implement the code so that you don't have inner Canvas elements. But not by binding to Layers. You would have to maintain a top-level collection of all of the MarkerClass elements, and bind to that. Please see below for an example of this.
I doubt you will see much difference in rendering performance, but note that the implementation trades XAML code for C# code. I.e. there's less XAML but a lot more C#. Maintaining the mirror collection of items certainly would add to overhead in your own code (though it's not outside the realm of possibility that WPF does something similar internally…I don't know), but that cost comes when adding and removing elements. Rendering them should be at least fast as in the nested collection scenario.
Note, however, that I doubt it would be noticeably faster. Deep hierarchies of UI elements is the norm in WPF, and the framework is optimized to handle that efficiently.
In any case, IMHO it is better to let the framework handle interpreting high-level abstractions. That's why we use higher-level languages and frameworks in the first place. Don't waste any time at all trying to "optimize" code if it takes you away from a better representation of the data you're modeling. Only pursue that if and when you have implemented the code the naïve way, it works 100% correctly, and you still have a measurable performance problem with a clear, achievable performance goal.
How to set Width and Height of inner Canvases, so they will have the same dimensions as main Canvas, without binding?
mainCanvas automatically fills all the space, but innerCanvases don't. ClipToBounds=True must be set on innerCanvases. Tried HorizontalAligment=Stretch but it is not working.
What is it you are trying to achieve by having the inner Canvas objects boundaries expand to fill the parent element? If you really need to do this, you should be able to bind the inner Canvas elements' Width and Height properties to the parent's ActualWidth and ActualHeight properties. But unless the inner Canvas is given some kind of formatting or something, you would not be able to see the actual Canvas object and I wouldn't expect the width and height of those elements to have any practical effect.
The overlapping: Okay, I think I missed something.
If I don't set Background at all it works fine, as it should. It was just funny for me that not setting Background doesn't work the same as Background=Transparent.
It makes sense to me that having no background fill at all would be different than having a background fill that has the alpha channel set to 0.
It would significantly complicate WPF's hit-testing code to have to check each pixel of each element under the mouse to see if that pixel is transparent. I suppose WPF could have special-cased the solid-brush fill scenario, but then people would complain that a solid-but-transparent brush disables hit-testing while other brushes with transparent pixels don't, even where they are transparent.
Note that you can format an object without having it participate in hit-testing. Just set its IsHitTestVisible property to False. Then it will can render on the screen, but will not respond to or interfere with mouse clicks.
Here's the code example of how you might implement the code using just a single Canvas object:
XAML:
<Window x:Class="TestSO33742236WpfNestedCollection.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:p="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:TestSO33742236WpfNestedCollection"
Loaded="Window_Loaded"
Title="MainWindow" Height="350" Width="525">
<ItemsControl ItemsSource="{Binding Path=Markers}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="LightBlue"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type c:MarkerClass}">
<Ellipse Width="20" Height="20" Fill="Black" MouseEnter="Ellipse_MouseEnter" MouseLeave="Ellipse_MouseLeave"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<p:Style>
<Setter Property="Canvas.Left" Value="{Binding Path=X}"></Setter>
<Setter Property="Canvas.Top" Value="{Binding Path=Y}"></Setter>
</p:Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
</Window>
C#:
class ViewModel
{
public ObservableCollection<MarkerClass> Markers { get; set; }
public ObservableCollection<LayerClass> Layers { get; set; }
public ViewModel()
{
Markers = new ObservableCollection<MarkerClass>();
Layers = new ObservableCollection<LayerClass>();
Layers.CollectionChanged += _LayerCollectionChanged;
for (int j = 0; j < 10; j++)
{
var Layer = new LayerClass();
for (int i = 0; i < 10; i++)
{
Layer.Markers.Add(new MarkerClass(i * 20, 10 * j));
}
Layers.Add(Layer);
}
}
private void _LayerCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
ObservableCollection<LayerClass> layers = (ObservableCollection<LayerClass>)sender;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
_InsertMarkers(layers, e.NewItems.Cast<LayerClass>(), e.NewStartingIndex);
break;
case NotifyCollectionChangedAction.Move:
case NotifyCollectionChangedAction.Replace:
_RemoveMarkers(layers, e.OldItems.Count, e.OldStartingIndex);
_InsertMarkers(layers, e.NewItems.Cast<LayerClass>(), e.NewStartingIndex);
break;
case NotifyCollectionChangedAction.Remove:
_RemoveMarkers(layers, e.OldItems.Count, e.OldStartingIndex);
break;
case NotifyCollectionChangedAction.Reset:
Markers.Clear();
break;
}
}
private void _RemoveMarkers(ObservableCollection<LayerClass> layers, int count, int removeAt)
{
int removeMarkersAt = _MarkerCountForLayerIndex(layers, removeAt);
while (count > 0)
{
LayerClass layer = layers[removeAt++];
layer.Markers.CollectionChanged -= _LayerMarkersCollectionChanged;
Markers.RemoveRange(removeMarkersAt, layer.Markers.Count);
}
}
private void _InsertMarkers(ObservableCollection<LayerClass> layers, IEnumerable<LayerClass> newLayers, int insertLayersAt)
{
int insertMarkersAt = _MarkerCountForLayerIndex(layers, insertLayersAt);
foreach (LayerClass layer in newLayers)
{
layer.Markers.CollectionChanged += _LayerMarkersCollectionChanged;
Markers.InsertRange(layer.Markers, insertMarkersAt);
insertMarkersAt += layer.Markers.Count;
}
}
private void _LayerMarkersCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
ObservableCollection<MarkerClass> markers = (ObservableCollection<MarkerClass>)sender;
int layerIndex = _GetLayerIndexForMarkers(markers);
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Markers.InsertRange(e.NewItems.Cast<MarkerClass>(), _MarkerCountForLayerIndex(Layers, layerIndex));
break;
case NotifyCollectionChangedAction.Move:
case NotifyCollectionChangedAction.Replace:
Markers.RemoveRange(layerIndex, e.OldItems.Count);
Markers.InsertRange(e.NewItems.Cast<MarkerClass>(), _MarkerCountForLayerIndex(Layers, layerIndex));
break;
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Reset:
Markers.RemoveRange(layerIndex, e.OldItems.Count);
break;
}
}
private int _GetLayerIndexForMarkers(ObservableCollection<MarkerClass> markers)
{
for (int i = 0; i < Layers.Count; i++)
{
if (Layers[i].Markers == markers)
{
return i;
}
}
throw new ArgumentException("No layer found with the given markers collection");
}
private static int _MarkerCountForLayerIndex(ObservableCollection<LayerClass> layers, int layerIndex)
{
return layers.Take(layerIndex).Sum(layer => layer.Markers.Count);
}
}
static class Extensions
{
public static void InsertRange<T>(this ObservableCollection<T> source, IEnumerable<T> items, int insertAt)
{
foreach (T t in items)
{
source.Insert(insertAt++, t);
}
}
public static void RemoveRange<T>(this ObservableCollection<T> source, int index, int count)
{
for (int i = index + count - 1; i >= index; i--)
{
source.RemoveAt(i);
}
}
}
Caveat: I have not tested the code above thoroughly. I've only run it in the context of the original code example, which only ever adds pre-populated LayerClass objects to the Layers collection, and so only the Add scenario has been tested. There could be typographical or even significant logic bugs, though of course I've tried to avoid that. As with any code you find on the Internet, use at your own risk. :)

Related

How can I implement a carousel of images in WPF where the selected item is always the first one?

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.

Different MouseMove behaviour with ItemsControl?

I have WPF form, and in XAML I have label which on holding MouseDown I can move around form and on MouseRelease leave it in new position.
This is working as expected.
XAML code:
<Canvas>
<Label Content="Label"
Background="ForestGreen"
Padding="12,7"
Canvas.Left="{Binding XPosition}"
Canvas.Top="{Binding YPosition}"
MouseDown="Label_MouseDown"
MouseUp="Label_MouseUp"
MouseMove="Label_MouseMove"/>
</Canvas>
C#
public partial class frmTables : Window, INotifyPropertyChanged
{
private Point BasePoint = new Point(0.0, 0.0);
private double DeltaX = 0.0;
private double DeltaY = 0.0;
private bool moving = false;
private Point PositionInLabel;
public frmTables()
{
InitializeComponent();
this.DataContext = this;
}
public double XPosition
{
get { return BasePoint.X + DeltaX; }
}
public double YPosition
{
get { return BasePoint.Y + DeltaY; }
}
private void Label_MouseDown(object sender, MouseButtonEventArgs e)
{
Label l = e.Source as Label;
if (l != null)
{
l.CaptureMouse();
moving = true;
PositionInLabel = e.GetPosition(l);
lblCoord.Content = "MouseDown";
}
}
private void Label_MouseMove(object sender, MouseEventArgs e)
{
if (moving)
{
Point p = e.GetPosition(null);
DeltaX = p.X - BasePoint.X - PositionInLabel.X;
DeltaY = p.Y - BasePoint.Y - PositionInLabel.Y;
RaisePropertyChanged("XPosition");
RaisePropertyChanged("YPosition");
lblCoord.Content = DeltaX + ":" + DeltaY;
}
}
private void Label_MouseUp(object sender, MouseButtonEventArgs e)
{
Label l = e.Source as Label;
if (l != null)
{
l.ReleaseMouseCapture();
BasePoint.X += DeltaX;
BasePoint.Y += DeltaY;
DeltaX = 0.0;
DeltaY = 0.0;
moving = false;
lblCoord.Content = BasePoint.X + ":" + BasePoint.Y;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string prop)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
}
}
This is all working as expected until I change XAML, and create two labels during runtime from code behind:
<Canvas>
<ItemsControl Name="btnTableImageList">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Label Content="Label"
Background="ForestGreen"
Padding="12,7"
Canvas.Left="{Binding XPosition}"
Canvas.Top="{Binding YPosition}"
MouseDown="Label_MouseDown"
MouseUp="Label_MouseUp"
MouseMove="Label_MouseMove"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
Everything else remains the same, except generating those labes from code behind and changed XAML. Now if I hold MouseDown on label and move, nothing happens but MouseDown, and MouseMove are working since I can see test messages in lblCoord.Content.
If you need I can show you label generation code, but it's nothing special, just a class with a for-loop to create certain number of labels and I am calling that on WindowLoaded with btnTableImageList.ItemsSource = tableLbl.CreateTableLabels();.
Anyone have idea why is this happening, or to be more precise, what am I doing wrong?
The standard ItemsPanel of an ItemsControl is a StackPanel, so you will not see any effect when changing Canvas.Left/Top. You can modify the ItemsPanel to provide a Canvas instead.
<Canvas>
<ItemsControl Name="btnTableImageList">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Label Content="Label"
Background="ForestGreen"
Padding="12,7"
Canvas.Left="{Binding XPosition}"
Canvas.Top="{Binding YPosition}"
MouseDown="Label_MouseDown"
MouseUp="Label_MouseUp"
MouseMove="Label_MouseMove"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
NOTE: there is still something missing; for a complete solution see Setting Canvas properties in an ItemsControl DataTemplate
I think what you're trying to do here is a big step beyond your first piece of code. Because it touches on a number of wpf concepts you probably haven't looked at.
Stackpanel is only part of the thing you need to understand here.
I recommend you download and install snoop.
When you run it you get a weird toolbar with some scope sight things on it.
Drag one over your window and a new window appears.
Hover over where one of your labels is and press shift+ctrl at the same time.
You will see the tree of controls.
Your label will be inside a contentpresenter and it is this which goes directly in the canvas. It is this you need to set canvas.top and canvas.left on.
You will notice when you try and manipulate that control that working in this was is a nuisance.
Why is this so hard? ( you are likely to think ).
That's because you're intended to use binding and templating rather than directly add controls.
Hence, set the datacontext of the window to a viewmodel, bind an observablecollection of viewmodels from there to the itemssource of the itemscontrol. Each of these latter viewmodels would then expose left and top properties which are used to position your piece of ui ( label ).
Here's a working sample illustrates the approach of binding and templating:
https://1drv.ms/u/s!AmPvL3r385QhgooJ94uO6PopIDs4lQ
That's templating into textboxes and stuff rather than doing exactly what you're trying to do.

Convert Shape into reusable Geometry in WPF

I am trying to convert a System.Windows.Shapes.Shape object into a System.Windows.Media.Geometry object.
With the Geometry object, I am going to render it multiple times with a custom graph control depending on a set of data points. This requires that each instance of the Geometry object has a unique TranslateTransform object.
Now, I am approaching the issue in two different ways, but neither seems to be working correctly. My custom control uses the following code in order to draw the geometry:
//Create an instance of the geometry the shape uses.
Geometry geo = DataPointShape.RenderedGeometry.Clone();
//Apply transformation.
TranslateTransform translation = new TranslateTransform(dataPoint.X, dataPoint.Y);
geo.Transform = translation;
//Create pen and draw geometry.
Pen shapePen = new Pen(DataPointShape.Stroke, DataPointShape.StrokeThickness);
dc.DrawGeometry(DataPointShape.Fill, shapePen, geo);
I have also tried the following alternate code:
//Create an instance of the geometry the shape uses.
Geometry geo = DataPointShape.RenderedGeometry;
//Apply transformation.
TranslateTransform translation = new TranslateTransform(dataPoint.X, dataPoint.Y);
dc.PushTransform(translation);
//Create pen and draw geometry.
Pen shapePen = new Pen(DataPointShape.Stroke, DataPointShape.StrokeThickness);
dc.DrawGeometry(DataPointShape.Fill, shapePen, geo);
dc.Pop(); //Undo translation.
The difference is that the second snippet doesn't clone or modify the Shape.RenderedGeometry property.
Oddly enough, I occasionally can view the geometry used for the data points in the WPF designer. However, the behavior is inconsistent and difficult to figure out how to make the geometry always appear. Also, when I execute my application, the data points never appear with the specified geometry.
EDIT: I have figured out how to generate the appearance of the geometry. But this only works in design-mode. Execute these steps:
Rebuild project.
Go to MainWindow.xaml and click in the custom shape object so that the shape's properties load into Visual Studio's property window. Wait until the property window renders what the shape looks like.
Modify the data points collection or properties to see the geometry rendered properly.
Here is what I want the control to ultimately look like for now:
How can I convert a Shape object to a Geometry object for rendering multiple times?
Your help is tremendously appreciated!
Let me give the full context of my problem, as well as all necessary code to understanding how my control is set up. Hopefully, this might indicate what problems exist in my method of converting the Shape object to a Geometry object.
MainWindow.xaml
<Window x:Class="CustomControls.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomControls">
<Grid>
<local:LineGraph>
<local:LineGraph.DataPointShape>
<Ellipse Width="10" Height="10" Fill="Red" Stroke="Black" StrokeThickness="1" />
</local:LineGraph.DataPointShape>
<local:LineGraph.DataPoints>
<local:DataPoint X="10" Y="10"/>
<local:DataPoint X="20" Y="20"/>
<local:DataPoint X="30" Y="30"/>
<local:DataPoint X="40" Y="40"/>
</local:LineGraph.DataPoints>
</local:LineGraph>
</Grid>
DataPoint.cs
This class just has two DependencyProperties (X & Y) and it gives a notification when any of those properties are changed. This notification is used to trigger a re-render via UIElement.InvalidateVisual().
public class DataPoint : DependencyObject, INotifyPropertyChanged
{
public static readonly DependencyProperty XProperty = DependencyProperty.Register("XProperty", typeof(double), typeof(DataPoint), new FrameworkPropertyMetadata(0.0d, DataPoint_PropertyChanged));
public static readonly DependencyProperty YProperty = DependencyProperty.Register("YProperty", typeof(double), typeof(DataPoint), new FrameworkPropertyMetadata(0.0d, DataPoint_PropertyChanged));
private static void DataPoint_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
DataPoint dp = (DataPoint)sender;
dp.RaisePropertyChanged(e.Property.Name);
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
public double X
{
get { return (double)GetValue(XProperty); }
set { SetValue(XProperty, (double)value); }
}
public double Y
{
get { return (double)GetValue(YProperty); }
set { SetValue(YProperty, (double)value); }
}
}
LineGraph.cs
This is the control. It contains the collection of data points and provides mechanisms for re-rendering the data points (useful for WPF designer). Of particular importance is the logic posted above which is inside of the UIElement.OnRender() method.
public class LineGraph : FrameworkElement
{
public static readonly DependencyProperty DataPointShapeProperty = DependencyProperty.Register("DataPointShapeProperty", typeof(Shape), typeof(LineGraph), new FrameworkPropertyMetadata(default(Shape), FrameworkPropertyMetadataOptions.AffectsRender, DataPointShapeChanged));
public static readonly DependencyProperty DataPointsProperty = DependencyProperty.Register("DataPointsProperty", typeof(ObservableCollection<DataPoint>), typeof(LineGraph), new FrameworkPropertyMetadata(default(ObservableCollection<DataPoint>), FrameworkPropertyMetadataOptions.AffectsRender, DataPointsChanged));
private static void DataPointShapeChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
LineGraph g = (LineGraph)sender;
g.InvalidateVisual();
}
private static void DataPointsChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{ //Collection referenced set or unset.
LineGraph g = (LineGraph)sender;
INotifyCollectionChanged oldValue = e.OldValue as INotifyCollectionChanged;
INotifyCollectionChanged newValue = e.NewValue as INotifyCollectionChanged;
if (oldValue != null)
oldValue.CollectionChanged -= g.DataPoints_CollectionChanged;
if (newValue != null)
newValue.CollectionChanged += g.DataPoints_CollectionChanged;
//Update the point visuals.
g.InvalidateVisual();
}
private void DataPoints_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{ //Collection changed (added/removed from).
if (e.OldItems != null)
foreach (INotifyPropertyChanged n in e.OldItems)
{
n.PropertyChanged -= DataPoint_PropertyChanged;
}
if (e.NewItems != null)
foreach (INotifyPropertyChanged n in e.NewItems)
{
n.PropertyChanged += DataPoint_PropertyChanged;
}
InvalidateVisual();
}
private void DataPoint_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
//Re-render the LineGraph when a DataPoint has a property that changes.
InvalidateVisual();
}
public Shape DataPointShape
{
get { return (Shape)GetValue(DataPointShapeProperty); }
set { SetValue(DataPointShapeProperty, (Shape)value); }
}
public ObservableCollection<DataPoint> DataPoints
{
get { return (ObservableCollection<DataPoint>)GetValue(DataPointsProperty); }
set { SetValue(DataPointsProperty, (ObservableCollection<DataPoint>)value); }
}
public LineGraph()
{ //Provide instance-specific value for data point collection instead of a shared static instance.
SetCurrentValue(DataPointsProperty, new ObservableCollection<DataPoint>());
}
protected override void OnRender(DrawingContext dc)
{
if (DataPointShape != null)
{
Pen shapePen = new Pen(DataPointShape.Stroke, DataPointShape.StrokeThickness);
foreach (DataPoint dp in DataPoints)
{
Geometry geo = DataPointShape.RenderedGeometry.Clone();
TranslateTransform translation = new TranslateTransform(dp.X, dp.Y);
geo.Transform = translation;
dc.DrawGeometry(DataPointShape.Fill, shapePen, geo);
}
}
}
}
EDIT 2:In response to this answer by Peter Duniho, I would like to provide the alternate method to lying to Visual Studio in creating a custom control. For creating the custom control execute these steps:
Create folder in root of project named Themes
Create resource dictionary in Themes folder named Generic.xaml
Create a style in the resource dictionary for the control.
Apply the style from the control's C# code.
Generic.xamlHere is an example of for the SimpleGraph described by Peter.
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomControls">
<Style TargetType="local:SimpleGraph" BasedOn="{StaticResource {x:Type ItemsControl}}">
<Style.Resources>
<EllipseGeometry x:Key="defaultGraphGeometry" Center="5,5" RadiusX="5" RadiusY="5"/>
</Style.Resources>
<Style.Setters>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate DataType="{x:Type local:DataPoint}">
<Path Fill="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SimpleGraph}}, Path=DataPointFill}"
Stroke="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SimpleGraph}}, Path=DataPointStroke}"
StrokeThickness="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SimpleGraph}}, Path=DataPointStrokeThickness}"
Data="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SimpleGraph}}, Path=DataPointGeometry}">
<Path.RenderTransform>
<TranslateTransform X="{Binding X}" Y="{Binding Y}"/>
</Path.RenderTransform>
</Path>
</DataTemplate>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>
</ResourceDictionary>
Lastly, apply the style like so in the SimpleGraph constructor:
public SimpleGraph()
{
DefaultStyleKey = typeof(SimpleGraph);
DataPointGeometry = (Geometry)FindResource("defaultGraphGeometry");
}
I think that you are probably not approaching this in the best way. Based on the code you posted, it seems that you are trying to do manually things that WPF is reasonably good at handling automatically.
The main tricky part (at least for me…I'm hardly a WPF expert) is that you appear to want to use an actual Shape object as the template for your graph's data point graphics, and I'm not entirely sure of the best way to allow for that template to be replaced programmatically or declaratively without exposing the underlying transformation mechanic that controls the positioning on the graph.
So here's an example that ignores that particular aspect (I will comment on alternatives below), but which I believe otherwise serves your precise needs.
First, I create a custom ItemsControl class (in Visual Studio, I do this by lying and telling VS I want to add a UserControl, which gets me a XAML-based item in the project…I immediately replace "UserControl" with "ItemsControl" in both the .xaml and .xaml.cs files):
XAML:
<ItemsControl x:Class="TestSO28332278SimpleGraphControl.SimpleGraph"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:TestSO28332278SimpleGraphControl"
mc:Ignorable="d"
x:Name="root"
d:DesignHeight="300" d:DesignWidth="300">
<ItemsControl.Resources>
<EllipseGeometry x:Key="defaultGraphGeometry" Center="5,5" RadiusX="5" RadiusY="5" />
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:DataPoint}">
<Path Data="{Binding ElementName=root, Path=DataPointGeometry}"
Fill="Red" Stroke="Black" StrokeThickness="1">
<Path.RenderTransform>
<TranslateTransform X="{Binding X}" Y="{Binding Y}"/>
</Path.RenderTransform>
</Path>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
C#:
public partial class SimpleGraph : ItemsControl
{
public Geometry DataPointGeometry
{
get { return (Geometry)GetValue(DataPointShapeProperty); }
set { SetValue(DataPointShapeProperty, value); }
}
public static DependencyProperty DataPointShapeProperty = DependencyProperty.Register(
"DataPointGeometry", typeof(Geometry), typeof(SimpleGraph));
public SimpleGraph()
{
InitializeComponent();
DataPointGeometry = (Geometry)FindResource("defaultGraphGeometry");
}
}
The key here is that I have an ItemsControl class with a default ItemTemplate that has a single Path object. That object's geometry is bound to the controls DataPointGeometry property, and its RenderTransform is bound to the data item's X and Y values as offsets for a translation transform.
A simple Canvas is used for the ItemsPanel, as I just need a place to draw things, without any other layout features. Finally, there is a resource defining a default geometry to use, in case the caller doesn't provide one.
And about that caller…
Here is a simple example of how one might use the above:
<Window x:Class="TestSO28332278SimpleGraphControl.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestSO28332278SimpleGraphControl"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<PathGeometry x:Key="dataPointGeometry"
Figures="M 0.5000,0.0000
L 0.6176,0.3382
0.9755,0.3455
0.6902,0.5618
0.7939,0.9045
0.5000,0.7000
0.2061,0.9045
0.3098,0.5618
0.0245,0.3455
0.3824,0.3382 Z">
<PathGeometry.Transform>
<ScaleTransform ScaleX="20" ScaleY="20" />
</PathGeometry.Transform>
</PathGeometry>
</Window.Resources>
<Grid>
<Border Margin="3" BorderBrush="Black" BorderThickness="1">
<local:SimpleGraph Width="450" Height="300" DataPointGeometry="{StaticResource dataPointGeometry}">
<local:SimpleGraph.Items>
<local:DataPoint X="10" Y="10" />
<local:DataPoint X="25" Y="25" />
<local:DataPoint X="40" Y="40" />
<local:DataPoint X="55" Y="55" />
</local:SimpleGraph.Items>
</local:SimpleGraph>
</Border>
</Grid>
</Window>
In the above, the only truly interesting thing is that I declare a PathGeometry resource, and then bind that resource to the control's DataPointGeometry property. This allows the program to provide a custom geometry for the graph.
WPF handles the rest through implicit data binding and templating. If the values of any of the DataPoint objects change, or the data collection itself is modified, the graph will be updated automatically.
Here's what it looks like:
I will note that the above example only allows you to specify the geometry. The other shape attributes are hard-coded in the data template. This seems slightly different from what you asked to do. But note that you have a few alternatives here that should address your need without requiring the reintroduction of all the extra manual-binding/updating code in your example:
Simply add other properties, bound to the template Path object in a fashion similar to the DataPointGeometry property. E.g. DataPointFill, DataPointStroke, etc.
Go ahead and allow the user to specify a Shape object, and then use the properties of that object to populate specific properties bound to the properties of the template object. This is mainly a convenience to the caller; if anything, it's a bit of added complication in the graph control itself.
Go whole-hog and allow the user to specify a Shape object, which you then convert to a template by using XamlWriter to create some XAML for the object, add the necessary Transform element to the XAML and wrap it in a DataTemplate declaration (e.g. by loading the XAML as an in-memory DOM to modify the XAML), and then using XamlReader to then load the XAML as a template which you can then assign to the ItemTemplate property.
Option #3 seems the most complicated to me. So complicated in fact that I did not bother to prototype an example using it…I did a little research and it seems to me that it should work, but I admit that I did not verify for myself that it does. But it would certainly be the gold standard in terms of absolute flexibility for the caller.

Treeview with multi colour node text

I have a project that requires the use of a treeview control. The control must have the ability for the text on each node to be formatted so the text can be multi coloured. This is best shown by the treeview used in outlook - see pic )
I historically have a windows forms control that I created to do this, my question is how easy is this to do in WPF without having to use 3rd party controls?
I historically have a windows forms control that I created to do this
Forget winforms, it's a dinosaur technology that has not been improved since 2007, it is not intended to create Rich UIs (only poor ones), and that does not support anything and forces you to write too much code and achieve less. It does not support any kind of customization and is slow as hell.
All the horrible hacks required in winforms to do anything (such as "owner draw" and "P/Invoke", whatever that means) are completely irrelevant and unneeded in WPF.
I dont really want to invest a lot of time moving of winforms to wpf if what I want to do is either not possible or too difficult
People are doing things like this in WPF, which are completely impossible in winforms, so what you're talking about here is really a "piece of cake" for WPF.
First of all, if you're getting into WPF, you must forget the traditional too-much-code-for-anything winforms approach and understand and embrace The WPF Mentality.
Here is how you implement that in WPF:
XAML:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<TreeView ItemsSource="{Binding}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<DockPanel>
<!-- this Image Control will display the "icon" for each Tree item -->
<Image Width="18" Height="16" Source="{Binding ImageSource}"
DockPanel.Dock="Left" Margin="2"/>
<!-- this TextBlock will show the main "Caption" for each Tree item -->
<TextBlock Text="{Binding DisplayName}" FontWeight="Bold"
VerticalAlignment="Center"
x:Name="DisplayName" Margin="2"/>
<!-- this TextBlock will show the Item count -->
<TextBlock Text="{Binding ItemCount, StringFormat='({0})'}"
VerticalAlignment="Center" Margin="2" x:Name="ItemCount">
<TextBlock.Foreground>
<SolidColorBrush Color="{Binding ItemCountColor}"/>
</TextBlock.Foreground>
</TextBlock>
</DockPanel>
<HierarchicalDataTemplate.Triggers>
<!-- This DataTrigger will hide the ItemCount text
and remove the Bold font weight from the DisplayName text
when ItemCount is zero -->
<DataTrigger Binding="{Binding ItemCount}" Value="0">
<Setter TargetName="ItemCount" Property="Visibility" Value="Collapsed"/>
<Setter TargetName="DisplayName" Property="FontWeight" Value="Normal"/>
</DataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Window>
Code Behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = DataSource.GetFolders();
}
}
Data Item:
public class Folder
{
public Folder(string displayName)
{
ImageSource = DataSource.Folder1;
Children = new List<Folder>();
ItemCountColor = "Blue";
DisplayName = displayName;
}
public Folder(string displayName, int itemCount): this(displayName)
{
ItemCount = itemCount;
}
public string DisplayName { get; set; }
public int ItemCount { get; set; }
public List<Folder> Children { get; set; }
public string ItemCountColor { get; set; }
public string ImageSource { get; set; }
}
DataSource class (A lot of boilerplate code that generates the tree entries and is not really part of the WPF side of things):
public static class DataSource
{
public const string Folder1 = "/folder1.png";
public const string Folder2 = "/folder2.png";
public const string Folder3 = "/folder3.png";
public const string Folder4 = "/folder4.png";
public static List<Folder> GetFolders()
{
return new List<Folder>
{
new Folder("Conversation History"),
new Folder("Deleted Items",102)
{
ImageSource = Folder2,
Children =
{
new Folder("Deleted Items #1"),
}
},
new Folder("Drafts",7)
{
ImageSource = Folder3,
ItemCountColor = "Green",
},
new Folder("Inbox",7)
{
ImageSource = Folder4,
Children =
{
new Folder("_file")
{
Children =
{
new Folder("__plans"),
new Folder("_CEN&ISO", 5),
new Folder("_DDMS", 1)
{
Children =
{
new Folder("Care Data Dictionary"),
new Folder("HPEN"),
new Folder("PR: Data Architecture"),
new Folder("PR: Hospital DS", 2),
new Folder("RDF"),
new Folder("Schemas"),
new Folder("Subsets"),
}
},
new Folder("_Interop")
{
Children =
{
new Folder("CDSA", 1),
new Folder("CPIS", 2),
new Folder("DMIC"),
new Folder("EOL"),
new Folder("... And so on..."),
}
}
}
}
}
}
};
}
}
Result:
As you can see, this full working sample consists of 30 lines of XAML, 1 line of C# code behind, and a simple POCO class that represents the folder structure, which consists of string, bool, int and List<T> properties and does not have any dependencies on the UI framework at all, plus the DataSource boilerplate, that does not have anything to do with WPF anyways.
Notice how my C# code is clean and simple and beautiful and does not have any horrible "owner draw" stuff or anything like that.
Also notice how the Data/Logic are completely decoupled from the UI, which gives you a HUGE amount of flexibility, scalability and maintainability. You could completely rewrite the UI into a totally different thing without changing a single line of C# code.
There is 1 line of Code behind, which sets the DataContext to a List<Folder>, the rest is achieved via DataBinding into the HierarchicalDataTemplate that defines the Visual structure of the Tree items.
This is the WPF way, to use DataBinding to simplify your life instead of a bunch of useless boilerplate piping to pass data between the UI and the Data Model.
Keep in mind that you can put literally anything inside the DataTemplate, not just text, not just read-only content. You can even put editable controls or even Video inside each tree item. the WPF Content Model does not suffer from the huge limitations imposed by other technologies.
WPF Rocks - just copy and paste my code in a File -> New Project -> WPF Application and see the results for yourself, you will need to add the png files and set their Build Action to Resource in your project:
Once you know WPF, XAML and MVVM, you will NEVER want to go back to winforms again, and you'll realize how much valuable time you've lost all these years using dead technology.

How to create a fast loading wrapping ListBox?

I've made a nice little three-item wide list of tiles that work as switches. It looks something like this:
Looking good huh? Well, I have about 130 of these tiles in a vertically scrolling list, and it takes ages to load. According to the performance analysis tool, each element takes about 18ms to render - which gives me about a 2.3 second rendering time. On the device, it's often twice that time. This wouldn't really be a crisis, but the UI is totally black and unresponsive up until these elements have been drawn.
After some research online, I realized this is because the WrapPanel control from the toolkit doesn't virtualize its items - thus making the GPU render all objects at once (using up a lot of memory in the process).
Now, are there any ways to make this go faster?
XAML:
<ListBox x:Name="ChannelsListBox" Grid.Row="2" Margin="0,40,0,0">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<toolkit:WrapPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.Template>
<ControlTemplate>
<ItemsPresenter />
</ControlTemplate>
</ListBox.Template>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid x:Name="ChannelTile" Margin="6,6,6,6" Tap="ChannelTile_Tap">
<!-- context menu code removed -->
<Rectangle Width="136" Height="136" Fill="{StaticResource LightGrayColor}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The ListBox's ItemsSource is set in the codebehind - if you wondered.
Well, if you populate the listbox asynchronously from another thread, you can avoid the unresponsive UI.
EDITED2:
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
/* In the xaml code:
<ListBox x:Name="ChannelsListBox" ItemsSource="{Binding ListOfTestClasses}" ...
*/
var vm = new MainPageViewModel();
DataContext = vm;
vm.StartLoadingDataAsync(10000);
}
}
public class MainPageViewModel
{
public ObservableCollection<TestClass> ListOfTestClasses { get; set; }
private BackgroundWorker workerThread;
public MainPageViewModel()
{
ListOfTestClasses = new ObservableCollection<TestClass>();
workerThread = new BackgroundWorker();
workerThread.DoWork += new DoWorkEventHandler((object sender, DoWorkEventArgs e) =>
{
for (int i = 0; i < (int)e.Argument; i++)
{
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
ListOfTestClasses.Add(new TestClass { Text = "Element " + (i + 1) });
});
Thread.Sleep(150);
}
});
}
public void StartLoadingDataAsync(int numberOfElements)
{
workerThread.RunWorkerAsync(numberOfElements);
}
}
public class TestClass
{
public string Text { get; set; }
}
A few ideas which might be helpful:
Find or implement a virtualizing WrapPanel. It's the most appropriate solution, but I don't think I've seen a solid implementation of one yet. But for this purpose, maybe you don't need perfection and can get away with something someone else has already written.
Use a parent virtualizing vertical StackPanel containing horizontal StackPanel children. To do this, you'd need to re-shape your single sequence of data into a shorter sequence of 3-item entries. However, that may not be too hard and should give you most of the benefits of the ideal solution.
Consider implementing "lazy" containers like I did for DeferredLoadListBox. The basic idea is to delay rendering containers until they show up on screen. I have more info and example code here: http://blogs.msdn.com/b/delay/archive/2010/09/08/never-do-today-what-you-can-put-off-till-tomorrow-deferredloadlistbox-and-stackpanel-help-windows-phone-7-lists-scroll-smoothly-and-consistently.aspx

Categories