How to create a fast loading wrapping ListBox? - c#

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

Related

c# - UWP ListView displays incorrect items upon rapid scrolling when it has a DataTemplate

I have a ListView that is intended to show every product within a database, and it works for the most part, but when I scroll down by dragging the scroll bar, the bottom items end up being incorrect.
XAML Definition:
<ListView x:Name="lst_Products" VerticalAlignment="Top" HorizontalAlignment="Left" Margin="16,124,16,16" Width="300" ContainerContentChanging="lst_Products_ContainerContentChanging" Loaded="lst_Products_Loaded" BorderBrush="Black" BorderThickness="2" CornerRadius="16">
<ListView.ItemTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding Value}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
The data template is present so I can easily grab a product ID number with SelectedValue. According to some trusted community member (or whatever they call the prominent posters) on the MSDN forums said that's the only way to properly show a ListView when the ItemsSource is an ObservableCollection<KeyValuePair<int,RelativePanel>> while having a selectable value member.
The relevant C# code:
private async void lst_Products_Loaded(object sender, RoutedEventArgs e)
{
var products = await ProductManager.GetProducts();
ObservableCollection<KeyValuePair<int, RelativePanel>> productList = new(products);
lst_Products.ItemsSource = productList;
lst_Products.SelectedValuePath = "Key";
}
private void lst_Products_ContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args)
{
if (args.ItemIndex % 2 == 1)
{
args.ItemContainer.Background = new SolidColorBrush(Color.FromArgb(128, 128, 128, 128));
}
else
{
args.ItemContainer.Background = UIManager.GetDefaultBackground();
}
}
public static async Task<List<KeyValuePair<int, RelativePanel>>> GetProducts()
{
var productPanels = new List<KeyValuePair<int, RelativePanel>>();
var productIDs = await SqlHandler.ReturnListQuery<int>($"SELECT id FROM {productTable}");
var productNames = await SqlHandler.ReturnListQuery<string>($"SELECT name FROM {productTable}");
var panels = new List<RelativePanel>();
foreach(var name in productNames)
{
RelativePanel panel = new();
TextBlock productName = new()
{
Text = name
};
panel.Children.Add(productName);
panels.Add(panel);
}
for(int i = 0; i < productIDs.Count; i++)
{
productPanels.Add(new KeyValuePair<int, string>(productIDs[i], panels[i]));
}
return productPanels;
}
The call to SQL Handler just runs an SQL query and returns a list of the results. I can post the code if you need, but I can assure you there's no sorting going on.
A screenshot of what the list looks like. The bottom item should be "Coffee" - Button Test Product 2 is the second item in the list.
A screenshot of the SQL datatable with the "Coffee" product at the bottom where it should be.
In this case it's just the bottom item that's incorrect, however other times it has jumbled 5 or 6 entries near the bottom. This only seems to occur with the DataTemplate/ContentPresenter, but without that, the RelativePanel does not display correctly in the list. Eventually the list will show more information about the product and as far as I can tell, there's no good way to do that without converting the SQL data into a RelativePanel on the c# side.
I'm open to suggestions on solving either the jumbling problem with the template, or adjusting the xaml so that I don't need the template to display bulk sql data without needing the template but I'm at a loss.
c# - UWP ListView displays incorrect items upon rapid scrolling when it has a DataTemplate
The problem should be caused by listview virtualization, There are two ways to sloved this prolbem, one is disalbe listview virtualization by setting ItemsPanel as StackPanel like the following
<ListView>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
And the other way is implement INotifyCollectionChanged interface for your model class. for more please refer to Data binding in depth
It's not good practice that useRelativePanel collection as datasoure, the better way is make RelativePanel in your DataTemplate and bind with mode class property.
For example
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Index}" />
<TextBlock Text="{Binding IsItem}" />
<Image Source="{Binding ImageSource}" Visibility="Collapsed" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>

Binding Collection of Collection to one Canvas Element

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. :)

Can I access XAML elements in an array in the codebehind?

I've been looking around but I haven't been able to find anything on this. I am trying to get started making Windows 8.1 apps in C# with Visual Studio 2013 Pro. I want to be able to access multiple elements (particularly buttons or text blocks) in an array because this is more convenient for developing things like board games. For instance, if I were developing tic-tac-toe, I might use a series of buttons like this:
<Grid>
<Button Name="Cell00"/>
<Button Name="Cell01"/>
<Button Name="Cell02"/>
<Button Name="Cell10"/>
<Button Name="Cell11"/>
<Button Name="Cell12"/>
<Button Name="Cell20"/>
<Button Name="Cell21"/>
<Button Name="Cell22"/>
<Grid/>
Now for the function that would check for a win, I would have to check all possible combinations like this is in the code behind:
private bool CheckForWin()
{
if((Cell00 == Cell01) && (Cell01 == Cell02) && isNotBlank(Cell02)) return true;
if((Cell10 == Cell11) && (Cell11 == Cell12) && isNotBlank(Cell12)) return true
...
return false; //if none of the win conditions pass
}
This type of code would be extremely cumbersome. I would like to write it instead in a way that lets me check the array with for loops.
I realize that with tic-tac-toe, it is fairly easy to code it using brute force, but this was the first example that came to my head. Other games like Reversi or Go would not work well like this because of either the sheer size or the fact that pieces placed can change other cells than the one they were placed on.
Any help with this would be greatly appreciated.
This is not the correct way to use WPF. WPF is designed to use data binding....creating and manipulating UI elements directly is bad form. There are more posts/discussion/questions about this than you can imagine and I'll leave you to research them for yourself. In the mean time this is how you use WPF "properly":
First use NuGet to add MVVM lite to your project so that you get the ViewModelBase class and create a view model for a single cell:
public class Cell : ViewModelBase
{
private string _Text;
public string Text
{
get { return _Text; }
set { _Text = value; RaisePropertyChanged(() => this.Text); }
}
}
One level up you'll want a main model to encapsulate an array of these, this is where you will typically do all your game logic:
public class MainModel : ViewModelBase
{
private ObservableCollection<Cell> _Cells;
public ObservableCollection<Cell> Cells
{
get { return _Cells; }
set { _Cells = value; RaisePropertyChanged(() => this.Cells); }
}
public MainModel()
{
this.Cells = new ObservableCollection<Cell>(
Enumerable.Range(1, 100)
.Select(i => new Cell { Text = i.ToString() })
);
}
}
Notice that all I'm doing at the moment is creating a 100-element collection of cells. This main view model becomes the one that you assign to your window's data context:
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainModel();
}
Now your XAML controls need to bind to this data. ItemsControl is used to render a collection of elements so use one of those and bind it to your array. You want them displayed in a 2D grid so replace the ItemsPanelTemplate with a WrapPanel. Finally add a DataTemplate for your Cell class so that a button gets drawn for each cell:
<Window.Resources>
<DataTemplate DataType="{x:Type local:Cell}">
<Button Width="32" Height="32" Content="{Binding Text}"/>
</DataTemplate>
</Window.Resources>
<ItemsControl ItemsSource="{Binding Cells}" Width="320" Height="320" HorizontalAlignment="Center" VerticalAlignment="Center">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
That's how you use WPF. Your logic is stored entirely in the view model and it's completely decoupled from the view. Here's what this particular code displays, it should be pretty self-evident how flexible this code is and easy to change:
That's very possible. Simply declare an array variable :
private Button[] _buttonArray;
populate the array once, maybe in constructor :
_buttonArray = new[] {Cell00, Cell01, .... , Cell22};
And all of the buttons are now accessible through _buttonArray.

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 emulate a console in WPF?

I'd like some tips-in-the-right-direction or even ready solutions to this problem and I'm pretty stuck (I'm just beginner/intermediate):
I'm trying to implement a SSH in my application. The SSH-backend works fine and such, but I'm stuck at the frontend. What WPF-Combination would present me with an adequate solution to emulate a console? Put aside a complete terminal-emulation, I'd be happy to simply readline/writeline into something that looks like a console :-)
My best approach yet was a 80x50 Grid of single characters resulting in 4000 single cells and that feels like a total overkill.
Another idea was to make a console-Appl. bound to a wpf-window in another project. But...is that even possible and how?
Given that you want to emulate a console, I'd do it like this. Note that you'd have to handle the commands and outputting the results yourself.
page.xaml
<Window x:Class="ConsoleEmulation.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" MinHeight="350" MinWidth="525" Height="350" Width="525">
<Grid>
<ScrollViewer Name="Scroller" Margin="0" Background="Black">
<StackPanel>
<ItemsControl ItemsSource="{Binding ConsoleOutput, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=.}" Foreground="White" FontFamily="Consolas"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBox Text="{Binding ConsoleInput, Mode=TwoWay}" Background="Black" Foreground="White" FontFamily="Consolas" Name="InputBlock" BorderBrush="{x:Null}" SelectionBrush="{x:Null}" />
</StackPanel>
</ScrollViewer>
</Grid>
</Window>
page.xaml.cs
public partial class MainWindow : Window
{
ConsoleContent dc = new ConsoleContent();
public MainWindow()
{
InitializeComponent();
DataContext = dc;
Loaded += MainWindow_Loaded;
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
InputBlock.KeyDown += InputBlock_KeyDown;
InputBlock.Focus();
}
void InputBlock_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
dc.ConsoleInput = InputBlock.Text;
dc.RunCommand();
InputBlock.Focus();
Scroller.ScrollToBottom();
}
}
}
public class ConsoleContent : INotifyPropertyChanged
{
string consoleInput = string.Empty;
ObservableCollection<string> consoleOutput = new ObservableCollection<string>() { "Console Emulation Sample..." };
public string ConsoleInput
{
get
{
return consoleInput;
}
set
{
consoleInput = value;
OnPropertyChanged("ConsoleInput");
}
}
public ObservableCollection<string> ConsoleOutput
{
get
{
return consoleOutput;
}
set
{
consoleOutput = value;
OnPropertyChanged("ConsoleOutput");
}
}
public void RunCommand()
{
ConsoleOutput.Add(ConsoleInput);
// do your stuff here.
ConsoleInput = String.Empty;
}
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged(string propertyName)
{
if (null != PropertyChanged)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Did you know that you can display a Console window from your application by using AllocConsole?
This is a simple way to create a "dual-mode" application can be a
console or windows forms application.
[DllImport("kernel32")]
static extern bool AllocConsole();
Or you can use this:
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="30"/>
</Grid.RowDefinitions>
<TextBlock Text="Console contents..." HorizontalAlignment="Stretch" VerticalAlignment="Stretch" x:Name="ConsoleTextBlock"/>
<DockPanel Grid.Row="1">
<TextBox/>
</DockPanel>
</Grid>
For better looks, replace the TextBlock with a ListBox and style the ItemTemplate accordingly.
I haven't done it myself, however it is one of my "I'll do it if I have time"-projects.
Thus I am still looking for an existing implementation :-P
Anyways some thoughts:
The applroach to use Visuals (i.e. Ellipses, Textblocks) is probably not a good Idea.
Just think of what has to happen if you want like 200x100 characters. Maybe even a backbuffer. Holding it all in memory + drawing it....it will be incredibly slow.
Therefore the better (or even right) approach is to "draw yourself". Since WPF is backbuffered and you don't want to display an arbitrary bitmap the most likly approach would be to create a new UserControl and override it's Paint-Method.
You ma prefer to derive from Control, but UserControl may have Content, so you can show something like a connection indicator icon inside.
Architecture-wise I'd suggest to create a dependecy property Buffer (ConsoleBuffer) that holds the console buffer-model. Another DP would hold the top-left positon Location (long). It determines where to start the display (while you have a look behind). The console model I would make a class that contains a char[] and a Color[] (one dimensional). Use line breaking and \n characters to make lines (because this is the character of a console). Then if you resize the control it will re-flow without the buffer needing to be re-allocated.
You can work with **ConsoleBuffer**s of different sizes (for a different number of look behind characters).
ConsoleBuffer.Write(string s) is your method to do stuff.
Maybe it is advisable to hold arrays of arrays char[][] to represent lines.... but that is up to finding out while programming.

Categories