I having problems adding an AvalonDock AnchorablesSource under a ViewModel first MVVM approach using Stylet.
My avalonDock XAML is as follows:
<DockingManager
Grid.Row="1"
DocumentsSource="{Binding Scl.Documents}"
AnchorablesSource="{Binding Scl.DocumentsAnchorable}"
x:Name="dockManager"
AllowMixedOrientation="True"
AutoWindowSizeWhenOpened="True"
IsVirtualizingAnchorable="True"
IsVirtualizingDocument="True"
ActiveContent="{Binding Scl.ActiveDocument, Mode=TwoWay}"
DocumentClosed="{s:Action DocumentClosed}"
>
<DockingManager.LayoutItemContainerStyle>
<Style TargetType="{x:Type LayoutItem}">
<Setter Property="Title" Value="{Binding Model.Title}"/>
<Setter Property="Height" Value="Auto"/>
<Setter Property="IconSource" Value="{Binding Model.IconSource}" />
</Style>
</DockingManager.LayoutItemContainerStyle>
<DockingManager.LayoutItemTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<ContentControl s:View.Model="{Binding Content , FallbackValue=#ERROR Content.title#}"></ContentControl>
</Grid>
</DataTemplate>
</DockingManager.LayoutItemTemplate>
<LayoutRoot x:Name="root">
<LayoutPanel Orientation="Horizontal">
<LayoutAnchorablePane x:Name="LayoutAnchorablePane" DockWidth="50">
</LayoutAnchorablePane>
<LayoutDocumentPaneGroup>
<LayoutDocumentPane x:Name="LayoutDocumentPane">
<!-- This is where the new windows are typically added -->
</LayoutDocumentPane>
</LayoutDocumentPaneGroup>
<LayoutAnchorablePaneGroup DockWidth="250">
<LayoutAnchorablePane x:Name="LayoutAnchorablePane1">
</LayoutAnchorablePane>
</LayoutAnchorablePaneGroup>
</LayoutPanel>
<LayoutRoot.LeftSide>
<LayoutAnchorSide>
<LayoutAnchorGroup>
</LayoutAnchorGroup>
</LayoutAnchorSide>
</LayoutRoot.LeftSide>
</LayoutRoot>
</DockingManager>
In my ViewModel I have:
public ObservableCollection<LayoutDocument> Documents {get; set;} = new ObservableCollection<LayoutDocument>();
public ObservableCollection<LayoutAnchorable> DocumentsAnchorable { get; set; } = new ObservableCollection<LayoutAnchorable>();
When I add a new normal layout to the LayoutDocumentPane as follows it works perfectly:
public void NewLayout(Screen viewModel, string title)
{
if (IsOpen(title) == true)
{
return;
}
LayoutDocument layout = new LayoutDocument
{
Title = title,
Content = viewModel
};
Documents.Add(layout);
Documents.Move(Documents.Count - 1, 0);
ActiveDocument = layout;
}
But If try and add a new Anchorable Layout (even with a know working ViewModel like this I get an error.
public void NewLayoutAnchorable(Screen viewModel, string title)
{
if (IsOpen(title) == true)
{
return;
}
LayoutAnchorable layout = new LayoutAnchorable
{
Title = title,
Content = viewModel
};
DocumentsAnchorable.Add(layout);
//DocumentsAnchorable.Move(Documents.Count - 1, 0);
//ActiveDocument = layout;
}
The error I get is:
Exception thrown: 'Stylet.StyletViewLocationException' in Stylet.dll
Exception thrown: 'System.Windows.Markup.XamlParseException' in
PresentationFramework.dll Unable to transform ViewModel name
AvalonDock.Layout.LayoutAnchorable into a suitable View name
Does anyone know why Stylet can find the relevant View then the ViewModel is used for a LayoutDocument but not a LayoutAnchorable in AvalonDock?
Edit 1:
The issue does not appear to be with the LayoutAnchorable viewmodels, as I can add them to the ObservableCollection<LayoutDocument> and they find a view just fine.
It is only a problem if I try and add a viewmodel to the ObservableCollection<LayoutAnchorable> that I get the error, so it appear to be an issue with my AvalonDock XAML.
Edit 2:
If I remove the following line from my XAML:
<ContentControl s:View.Model="{Binding Content , FallbackValue=#ERROR Content.title#}"></ContentControl>
I can add ViewModels to both by observable collections, and the windows appear in both the LayoutPane and the AchorablePane, except the windows are missing their content.
It therefore appears that I need a LayoutItemTemplate that works for anchorable windows but I can't seem to find an example.
Edit 3:
I've made a bare bones project that demonstrates the problem here if anybody wants to have a play.
https://github.com/montyjohn/StyletAvalonDockTest.git
If Stylet and AvalonDock can be made to play nicely together it would be a great starting point so new applications.
I think there is a problem with DockingManager, because it sets different DataContexts for its items. The DataContext for ContentControl is LayoutDocument for DocumentPane but it is ContentPresenter (Parent of LayoutAnchorable) for AnchorablePane.
You can use this workaround. I added a template selector and modified ShellView.xaml file. Now it works.
ShellView.xaml
<Window x:Class="StyletAvalonDockTest.Views.ShellView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ts="clr-namespace:StyletAvalonDockTest.TemplateSelectors"
xmlns:s="https://github.com/canton7/Stylet"
mc:Ignorable="d"
Title="ShellView" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<Button Width="auto" Command="{s:Action NewLayout}">Add New Window</Button>
<Button Width="auto" Command="{s:Action NewLayoutAnchorable}">Add New Anchorable Pane</Button>
</StackPanel>
<DockingManager
Grid.Row="1"
DocumentsSource="{Binding Documents}"
AnchorablesSource="{Binding DocumentsAnchorable}"
>
<!--If the theme is removed, neither the new Layout, nor the New Anchorable Layout work-->
<DockingManager.Theme>
<Vs2013LightTheme />
</DockingManager.Theme>
<!--This adds the title to the new windows-->
<DockingManager.LayoutItemContainerStyle>
<Style TargetType="{x:Type LayoutItem}">
<Setter Property="Title" Value="{Binding Model.Title}"/>
</Style>
</DockingManager.LayoutItemContainerStyle>
<DockingManager.LayoutItemTemplateSelector>
<ts:PanesTemplateSelector>
<ts:PanesTemplateSelector.DocumentPaneTemplate>
<DataTemplate>
<ContentControl s:View.Model="{Binding Content}"/>
</DataTemplate>
</ts:PanesTemplateSelector.DocumentPaneTemplate>
<ts:PanesTemplateSelector.AnchoroblePaneTemplate>
<DataTemplate>
<ContentControl s:View.Model="{Binding Content.Content}"/>
</DataTemplate>
</ts:PanesTemplateSelector.AnchoroblePaneTemplate>
</ts:PanesTemplateSelector>
</DockingManager.LayoutItemTemplateSelector>
<LayoutRoot>
<LayoutPanel>
<LayoutAnchorablePane>
<!-- This is where the new Anchorable windows are added -->
</LayoutAnchorablePane>
<LayoutDocumentPaneGroup>
<LayoutDocumentPane>
<!-- This is where the new windows are added -->
</LayoutDocumentPane>
</LayoutDocumentPaneGroup>
</LayoutPanel>
</LayoutRoot>
</DockingManager>
</Grid>
PanesTemplateSelector.cs
using AvalonDock.Layout;
using System.Windows;
using System.Windows.Controls;
namespace StyletAvalonDockTest.TemplateSelectors
{
public class PanesTemplateSelector : DataTemplateSelector
{
public DataTemplate DocumentPaneTemplate { get; set; }
public DataTemplate AnchoroblePaneTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (item is LayoutDocument)
return DocumentPaneTemplate;
else
return AnchoroblePaneTemplate;
}
}
}
Related
I've been searching for a while and I just cannot find what I'm doing wrong.
I have a list of names that I show in a View, in my View I created an itemsControl, the ItemsSource is set to the observableCollection in the ViewModel. The goal is to give a overview of available names in an nxn table. The user should be able to filter the results with the searchbox on top.
First I tried this using a List of Lists which did not work as the view was not being updated according to the string in the searchbox. I found that I should actually use a ObservableCollection because it implements INotifyCollectionChanged. Below I try to implement an ObservableCollection but I fail to update the view when this collection changes.
View (see ItemsControl in the Xaml section):
Xaml Resources
<UserControl.Resources>
<DataTemplate x:Key="DataTemplate_Level2">
<Button Content="{Binding}" Height="150" Width="150" Margin="20,20,20,20">
<Button.Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Background" Value="#2ED99A"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#2EB9D9"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="#333f4a"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</DataTemplate>
<DataTemplate x:Key="DataTemplate_Level1">
<ItemsControl ItemsSource="{Binding}" ItemTemplate="{DynamicResource DataTemplate_Level2}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
</UserControl.Resources>
Xaml
<Grid Margin="10,10,10,10" VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Grid.Column="0" HorizontalAlignment="Left">
<TextBlock Text="{Binding HcfOverviewModel.NSharedModules, StringFormat=Shared Circuits: {0}}"></TextBlock>
<TextBlock Margin="30,0,0,30" Text="{Binding HcfOverviewModel.NPrivateModules, StringFormat=Private Circuits: {0}}"></TextBlock>
</StackPanel>
<Button Grid.Column="1" VerticalAlignment="Top" MaxHeight="20" Content="See Private Circuits"></Button>
</Grid>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Hidden">
<ItemsControl HorizontalAlignment="Center" VerticalAlignment="Top" ItemsSource="{Binding Modules, UpdateSourceTrigger=PropertyChanged}" ItemTemplate="{DynamicResource DataTemplate_Level1}"/>
</ScrollViewer>
</Grid>
ViewModel:
class HcfOverviewViewModel:BaseViewModel
{
private HcfOverviewModel _hcfOverviewModel;
private string _pathToSharedModules;
//private List<List<string>> _modules;
private ObservableCollection<ObservableCollection<string>> _modules;
public HcfOverviewViewModel()
{
_pathToSharedModules = #"C:\Users\scamphyn\source\repos\TSD\TSD\TempTestFolder";
_hcfOverviewModel = new HcfOverviewModel();
_hcfOverviewModel.NSharedModules = CheckDirectory.FindModules(_pathToSharedModules).Length;
_hcfOverviewModel.SharedModuleNames = CleanModuleNames(CheckDirectory.FindModules(_pathToSharedModules));
//_modules = CreateGridLayout(_hcfOverviewModel.SharedModuleNames);
_modules = CreateGrid(_hcfOverviewModel.SharedModuleNames);
}
public HcfOverviewModel HcfOverviewModel
{
get => _hcfOverviewModel;
}
public void Filter(string searchString)
{
Console.WriteLine(searchString);
if (searchString == "")
{
//Modules = CreateGridLayout(_hcfOverviewModel.SharedModuleNames);
Modules = CreateGrid(_hcfOverviewModel.SharedModuleNames);
}
else
{
Modules.Clear();
string[] filteredNames = _hcfOverviewModel.SharedModuleNames.Where(n => n.Contains(searchString)).ToArray();
//Modules = CreateGridLayout(filteredNames);
Modules = CreateGrid(filteredNames);
}
}
private string[] CleanModuleNames(string[] listToClean)
{
List<string> moduleNames = new List<string>();
foreach (string s in listToClean)
{
string[] listPath = s.Split('\\');
string name = listPath[listPath.Length - 1];
moduleNames.Add(name);
}
return moduleNames.ToArray();
}
public ObservableCollection<ObservableCollection<string>> CreateGrid(string[] moduleNames)
{
ObservableCollection<ObservableCollection<string>> lsts = new ObservableCollection<ObservableCollection<string>>();
int circuitsPerRow = 4;
int _nRows = moduleNames.Length / circuitsPerRow;
Queue<string> modules = new Queue<string>(moduleNames);
for(int i = 0; i <= _nRows; i++)
{
lsts.Add(new ObservableCollection<string>());
for (int j = 0; j < circuitsPerRow; j++)
{
if (modules.Count != 0)
{
lsts[i].Add(modules.Dequeue());
}
else
{
break;
}
}
}
return lsts;
}
public List<List<string>> CreateGridLayout(string[] moduleNames)
//To Create the grid in HcfOverview
{
List<List<string>> lsts = new List<List<string>>(); // create list of list to store nxn matrix data in.
int circuitsPerRow = 4;
//Determine correct size nxn
int _nRows = moduleNames.Length / circuitsPerRow;
//Create Queue
Queue<string> modules = new Queue<string>(moduleNames);
//Create data for ItemControls
for (int i = 0; i <= _nRows; i++) // number of rows
{
lsts.Add(new List<string>());
for (int j = 0; j < circuitsPerRow; j++) // number of columns
{
if (modules.Count != 0)
{
lsts[i].Add(modules.Dequeue());
}
else {
break;
}
}
}
return lsts;
}
public ObservableCollection<ObservableCollection<string>> Modules
{
get => _modules;
set {
_modules = value;
OnPropertyChanged("Modules");
}
}
}
"_modules" which is my ObservableCollection is populated in the CreateGrid Method.
Here is what it looks like in the window:
The search box is in the "parent" View/ViewModel when I detect an update there I call the filter method in my ViewModel shown above to update the ObservableCollection.
How can this be resoloved?
Update:
Because I've been stuck on this issue for week now I decided to create a new "Project" and check if I could get it to work in a simplified project.
In this simplified project I managed to add and remove rows. I used the exact same methods as I was using before. So the binding and updating of my observablecollection is done right. I now wonder if this issue would be in how I load this UserControl. The UserControl displaying this data is loaded through another View. see code below:
<ContentControl Grid.Row="2" Content="{Binding CurrentHcfViewModel}">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type ViewModels:HcfOverviewViewModel}">
<views:UserControlHcfOverview/>
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
My HcfOverviewViewModel is loaded into a usercontrol that exists in a different view. Could this have influence on the updating of the ObeservableCollection and its View in the HcfOverviewViewModel?
I found that I should actually use a ObservableCollection because it implements INotifyCollectionChanged.
Due to how you do your update its not working as you believe. When one changes the reference from one ObservableCollection to another ObservableCollection, that does not send a INotifyCollectionChanged message. Only add/delete/clear calls generate that message on an active referenced collection.
When you reset the reference you are telling the ItemsControl to fully clear itself and then show what is being cleared.
You need to not change the initial Modules collection after it is created. There are other issues with your logic that cascade from that and unless someone rewrites your custom control/logic, this question cannot be fully answered.
Regardless you need to start with not changing the references and then work on the changing internal collection items in the same pattern to the parent ObservableCollection.
Note, some controls need a Null to be set when one is setting to a new list. Its unclear if that is also compounding the issue, for you immediately change to the newer reference and the new reference top level does not seem to change the whole ItemsControl.
It's a way to modify the height on the window until remains only the window bar? Right now I use to set the height of the window to 0 BUT still remain some content above the window bar (that white and gray area), I want to remove this completely and leave only the window bar:
I use MVVM so binding is needed:
XAML:
Width="{Binding MainWindowWidthSize, Mode=TwoWay}"
Height="{Binding MainWindowHeightSize, Mode=TwoWay}"
C#
public void TriggerFloatingMode(object obj)
{
if (!_isFloatingModeEnabled)
{
MainWindowWidthSize = 500;
MainWindowHeightSize = 0;
_isFloatingModeEnabled = true;
}
else
{
MainWindowWidthSize = 1000;
MainWindowHeightSize = 560;
_isFloatingModeEnabled = false;
}
}
Add this code WindowStyle="None" in the Window tag and enter the following code at the bottom of the Window tag
<Window.Resources>
<Style TargetType="{x:Type local:MainWindow}">
<Setter Property="WindowChrome.WindowChrome">
<Setter.Value>
<WindowChrome CornerRadius="0" GlassFrameThickness="0" ResizeBorderThickness="2" CaptionHeight="0"></WindowChrome>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
In the above code, change TargetType="{x:Type local:MainWindow}" to the name of your window local: your window name
An easy solution with your code could be
<Window x:Class="YourWindow.MainWindow"
SizeToContent="WidthAndHeight">
<Grid Height="{Binding MainWindowHeightSize }" Width="{Binding MainWindowWidthSize }">
</Grid>
</Window>
I have a WPF Grid which is 3 columns wide and 8 rows:
<Window x:Class="Container.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="700" Width="1000">
<Grid ShowGridLines="True">
<Grid.RowDefinitions>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition Height="20"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="10*" />
</Grid.ColumnDefinitions>
</Grid>
</Window>
I am using this to draw something like this:
Every cell in the first and third columns may have a different number of rectangles. Also, the width of each rectangle may be different and change at run-time. The width will be proportionate to a number (known at run-time and continually-changing).
What is the best way to draw these rectangles?
Here is what I've come up with after about an hour of fiddling (GitHub Repo):
I'm using the MVVM pattern to make the UI as easy as possible. Right now, it just populates with some random data.
The XAML:
<Window
x:Class="BuySellOrders.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:BuySellOrders"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="800"
Height="450"
mc:Ignorable="d">
<Window.DataContext>
<local:MainWindowVm />
</Window.DataContext>
<Grid Margin="15">
<ItemsControl ItemsSource="{Binding Path=Prices}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="1" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:PriceEntryVm}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border
Grid.Column="0"
Padding="5"
HorizontalAlignment="Stretch"
BorderBrush="Black"
BorderThickness="1">
<ItemsControl HorizontalAlignment="Right" ItemsSource="{Binding Path=BuyOrders}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:OrderVm}">
<Border
Width="{Binding Path=Qty}"
Margin="5"
Background="red"
BorderBrush="Black"
BorderThickness="1" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
<Border
Grid.Column="1"
BorderBrush="Black"
BorderThickness="1">
<TextBlock
Margin="8"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Path=Price}" />
</Border>
<Border
Grid.Column="2"
Padding="5"
BorderBrush="Black"
BorderThickness="1">
<ItemsControl ItemsSource="{Binding Path=SellOrders}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:OrderVm}">
<Border
Width="{Binding Path=Qty}"
Margin="5"
Background="red"
BorderBrush="Black"
BorderThickness="1" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window>
The view models:
class MainWindowVm : ViewModel
{
public MainWindowVm()
{
var rnd = new Random();
Prices = new ObservableCollection<PriceEntryVm>();
for (int i = 0; i < 8; i++)
{
var entry = new PriceEntryVm();
Prices.Add(entry);
entry.BuyOrders.CollectionChanged += OnOrderChanged;
entry.SellOrders.CollectionChanged += OnOrderChanged;
entry.Price = (decimal)110.91 + (decimal)i / 100;
var numBuy = rnd.Next(5);
for (int orderIndex = 0; orderIndex < numBuy; orderIndex++)
{
var order = new OrderVm();
order.Qty = rnd.Next(70) + 5;
entry.BuyOrders.Add(order);
}
var numSell = rnd.Next(5);
for (int orderIOndex = 0; orderIOndex < numSell; orderIOndex++)
{
var order = new OrderVm();
order.Qty = rnd.Next(70) + 5;
entry.SellOrders.Add(order);
}
}
}
private void OnOrderChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (var item in e.NewItems)
{
var order = item as OrderVm;
if (order.Qty > LargestOrder)
{
LargestOrder = order.Qty;
}
}
}
}
private int _largestOrder;
public int LargestOrder
{
get { return _largestOrder; }
private set { SetValue(ref _largestOrder, value); }
}
public ObservableCollection<PriceEntryVm> Prices { get; }
}
public class PriceEntryVm: ViewModel
{
public PriceEntryVm()
{
BuyOrders = new OrderList(this);
SellOrders = new OrderList(this);
}
private Decimal _price;
public Decimal Price
{
get {return _price;}
set {SetValue(ref _price, value);}
}
public OrderList BuyOrders { get; }
public OrderList SellOrders { get; }
}
public class OrderList : ObservableCollection<OrderVm>
{
public OrderList(PriceEntryVm priceEntry)
{
PriceEntry = priceEntry;
}
public PriceEntryVm PriceEntry { get; }
}
public class OrderVm : ViewModel
{
private int _qty;
public int Qty
{
get { return _qty; }
set { SetValue(ref _qty, value); }
}
}
I had to make some assumptions about the naming of things, but hopefully you should get the basic idea of what's going on.
It's structured as a list of PriceEntry, each of which contains a Price, and a BuyOrders and SellOrders properties.
BuyOrders and SellOrders are just lists of orders that have a Quantity property.
The XAML binds the list of price entries to a template that contains a 3 column grid. The first and 3rd columns of that grid bound to another set of item controls for each list of orders. The template for each order is just a border with a Width bound to the Quantity of the order.
All the binds means that just updating a property, or adding an order to either the buy or sell list of a price entry will automatically propagate to the UI. Adding or removing a PriceEntry will also automatically adjust the UI.
I haven't implemented your automatic scaling yet, but the basic idea would be to use a ValueConverter on the Quantity binding, to make it automatically adjust to the largest order.
As an extra note, it uses this nuget package to provide some of the MVVM boiler-plate code, but you should be able to use anything you want, as long as it gives you INotifyPropertyChanged support.
Here is a bonus screen capture showing the dynamic nature of MVVM updating the UI based on a timer.
This only needed a few lines of code to randomly pick a row, then randomly pick an order on the row, then add or subtract a small random amount from the quantity.
_updateTimer = new DispatcherTimer();
_updateTimer.Tick += OnUpdate;
_updateTimer.Interval = TimeSpan.FromSeconds(0.01);
_updateTimer.Start();
private void OnUpdate(object sender, EventArgs e)
{
var entryIndex = _rnd.Next(Prices.Count);
var entry = Prices[entryIndex];
OrderList list;
list = _rnd.Next(2) == 1 ?
entry.BuyOrders :
entry.SellOrders;
if (list.Any())
{
var order = list[_rnd.Next(list.Count)];
order.Qty += _rnd.Next(0, 8) - 4;
}
}
Right then, here goes....
This is exactly the kind of thing you want to use data-binding for. You can try and do things manually if you like, but your code will quickly become very messy if you do. WPF lets you do things the old-school way (i.e. similar to WinForms et al) but that was really to facilitate porting of legacy code. I won't go into too much detail about MVVM (plenty of info on the net about it), but you can get started by using NuGet to add MVVMLightLibs or some other MVVM framework to your project and then you assign your main window a view model by doing something like this:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainViewModel();
}
}
So now it's time for the view model itself, which is a model of the data structures that you want your view to display:
public class MainViewModel : ViewModelBase
{
public ObservableCollection<PriceLevel> PriceLevels { get; } = new ObservableCollection<PriceLevel>
{
new PriceLevel(110.98, new int[]{ }, new int[]{ }),
new PriceLevel(110.97, new int[]{ }, new int[]{ }),
new PriceLevel(110.96, new int[]{ }, new int[]{ }),
new PriceLevel(110.95, new int[]{ }, new int[]{ 5 }),
new PriceLevel(110.94, new int[]{ }, new int[]{ 3, 8 }),
new PriceLevel(110.93, new int[]{ 8, 3, 5, }, new int[]{ }),
new PriceLevel(110.92, new int[]{ 3 }, new int[]{ }),
new PriceLevel(110.91, new int[]{ }, new int[]{ }),
};
}
public class PriceLevel
{
public double Price { get; }
public ObservableCollection<int> BuyOrders { get; }
public ObservableCollection<int> SellOrders { get; }
public PriceLevel(double price, IEnumerable<int> buyOrders, IEnumerable<int> sellOrders)
{
this.Price = price;
this.BuyOrders = new ObservableCollection<int>(buyOrders);
this.SellOrders = new ObservableCollection<int>(sellOrders);
}
}
If you don't already know, ObservableCollection is very similar to list but it propegrates change notification, so when you make your view display the data in it your GUI will update automatically whenever the list changes. This MainViewModel class contains an ObservableCollection of type PriceLevel, and each PriceLevel contains the price and the lists of buy and sell orders. This means you'll be able to add and remove price points, and also add and remove the orders in the price points, and your front-end will reflect those changes.
So on to the front end itself:
<Window.Resources>
<!-- Style to display order list as horizontal list of red rectangles -->
<Style x:Key="OrderListStyle" TargetType="{x:Type ItemsControl}">
<!-- Set ItemsPanel to a horizontal StackPanel -->
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<!-- Display each item in the order list as a red rectangle and scale x by 8*size -->
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<Border BorderBrush="Black" BorderThickness="1" Margin="5" >
<Rectangle Width="{Binding}" Height="20" Fill="Red">
<Rectangle.LayoutTransform>
<ScaleTransform ScaleX="8" ScaleY="1" />
</Rectangle.LayoutTransform>
</Rectangle>
</Border>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Style to make Price cells vertically aligned -->
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type DataGridCell}">
<Grid Background="{TemplateBinding Background}">
<ContentPresenter VerticalAlignment="Center" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- This style centers the column's header text -->
<Style TargetType="DataGridColumnHeader">
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
</Window.Resources>
<!-- This datagrid displays the main list of PriceLevels -->
<DataGrid ItemsSource="{Binding PriceLevels}" AutoGenerateColumns="False" IsReadOnly="True"
CanUserAddRows="False" CanUserDeleteRows="False" CanUserReorderColumns="False" CanUserResizeColumns="False"
CanUserResizeRows="False" CanUserSortColumns="False" RowHeight="30">
<DataGrid.Columns>
<!-- The buy orders column -->
<DataGridTemplateColumn Header="Buy orders" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding BuyOrders}" Style="{StaticResource OrderListStyle}" HorizontalAlignment="Right" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<!-- The price column -->
<DataGridTextColumn Header="Price" Width="Auto" Binding="{Binding Price}" />
<!-- The sell orders column -->
<DataGridTemplateColumn Header="Sell Orders" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding SellOrders}" Style="{StaticResource OrderListStyle}" HorizontalAlignment="Left" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
Looks a bit full-on, but if you break it down into sections it's actually pretty straightforward. The main different between this and what you've been trying to do is that I'm using a DataGrid. This is basically a Grid control that's had extra functionality added to make it respond dynamically to data that it's been bound to. It also has a lot of extra stuff we dont' need (editing, column resize/reordering etc) so I've turned all that off. The DataGrid binds to PriceLevels in the view model, so it will display a vertical list showing each one. I've then explicitly declared the 3 columns you're after. The middle one is easy, it's just text, so DataGridTextColumn will, do the job. The other two are horizontal arrays of rectangles, so I've used DataGridTemplateColumn which allows me to customize exactly how they look. This customization is mostly done in the OrderListStyle at the very top of the XAML which sets ItemsPanel to a horizontal StackPanel and sets ItemTemplate to a rectangle. There's also a bit of XAML in there to scale the rectangle by a constant, according to the value of the integer it's displaying in the order list.
Here's the result:
I know the XAML might seem a little full-on, but keep in mind this is now fully data-bound to that view model and it will automatically update in response to changes. This little bit of extra work at the start results in MUCH cleaner update code which is also easier to test and debug.
Hope this is what you're after, if you have any questions let me know and we can take it into chat.
UPDATE: If you want to see the dynamic update in action then add this to your main view model's constructor, it just adds and removes orders randomly:
public MainViewModel()
{
var rng = new Random();
var timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromSeconds(0.1);
timer.Tick += (s, e) =>
{
var row = this.PriceLevels[rng.Next(this.PriceLevels.Count())]; // get random row
switch (rng.Next(4))
{
case 0: row.BuyOrders.Add(1 + rng.Next(5)); break;
case 1: row.SellOrders.Add(1 + rng.Next(5)); break;
case 2: if (row.BuyOrders.Count() > 0) row.BuyOrders.RemoveAt(rng.Next(row.BuyOrders.Count())); break;
case 3: if (row.SellOrders.Count() > 0) row.SellOrders.RemoveAt(rng.Next(row.SellOrders.Count())); break;
}
};
timer.Start();
}
I'm sure I'm missing something really silly and stupid here, and I'll probably kick myself when I see it, but I just have a simple question.
I've got some code in the constructor of the code-behind for a view with a grid, that does the following:
Grid mainGrid = this.Content as Grid;
MenuItem item = mainGrid.ContextMenu.Items[0] as MenuItem;
this.ApplySkinFromMenuItem(item);
So my question is how can I do this from the ViewModel? The ViewModel doesn't know what "this" is, and doesn't have a reference to "this" either.
It is my understanding that the view model object is created in XAML by calling:
<ObjectDataProvider x:Key="TimersHostViewModel" ObjectType="{x:Type local:TimersHostViewModel}"/>
And setting the data context like so:
<Grid DataContext="{StaticResource TimersHostViewModel}" Style="{DynamicResource styleBackground}">
But this doesn't give the TimersHostViewModel any knowledge about "this.Content", and saying TimersHost.Content doesn't help, because TimersHost isn't an actual object, but a class, and I need an actual object to get the ".Content" from, and it should be the right object, the object that is from the code behind, but how can I get that into the view model?
After all following MVVM means that the ViewModel shouldn't have any knowledge about the View, and the View shouldn't have any knowledge about the ViewModel, and they just communicate back and forth with bindings and INotifyPropertyChanged and other such message passing techniques. I've done a fair bit of this stuff in another application, so I'm some-what familiar with the basics, but still some-what new, and still learning and even re-learning.
I've included the full source below. As you can see I'm in the process of trying to get the code out of the code behind and into the ViewModel, but I'm running into a compiler error when attempting to get this.Content from the main grid.
XAML:
<Window
x:Class="TimersXP.TimersHost"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
xmlns:local="clr-namespace:TimersXP"
Name="TimersHostView"
SizeToContent="Height"
Title="TimersXP"
WindowStartupLocation="CenterScreen"
WindowStyle="ToolWindow">
<Window.Resources>
<ObjectDataProvider x:Key="TimersHostViewModel" ObjectType="{x:Type local:TimersHostViewModel}"/>
</Window.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="21"/>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="90"/>
</Grid.ColumnDefinitions>
<!--Main Menu-->
<Menu IsMainMenu="True" Style="{DynamicResource styleBanner}" Margin="0,0,0,1">
<MenuItem Header="_Help" Style="{DynamicResource styleBanner}">
<MenuItem Header="_About" Style="{DynamicResource styleBanner}"/>
</MenuItem>
</Menu>
<!--Top Most Check Box-->
<CheckBox Content="Top Most" Grid.Column="1" Height="16" HorizontalAlignment="Left" Margin="11,2,0,0" Name="checkBox1" VerticalAlignment="Top" />
<!--Stopwatch & Countdown Tab Defintions-->
<TabControl Grid.Row="1" Grid.ColumnSpan="2" Style="{DynamicResource styleContentArea}">
<TabItem Header="Stopwatch"/>
<TabItem Header="Countdown"/>
</TabControl>
<!-- CONTEXT MENU -->
<Grid.ContextMenu>
<ContextMenu Style="{DynamicResource styleBanner}" MenuItem.Click="OnMenuItemClick">
<MenuItem Tag=".\Resources\Skins\BlackSkin.xaml" IsChecked="True">
<MenuItem.Header>
<Rectangle Width="120" Height="40" Fill="Black" />
</MenuItem.Header>
</MenuItem>
<MenuItem Tag=".\Resources\Skins\GreenSkin.xaml">
<MenuItem.Header>
<Rectangle Width="120" Height="40" Fill="Green" />
</MenuItem.Header>
</MenuItem>
<MenuItem Tag=".\Resources\Skins\BlueSkin.xaml">
<MenuItem.Header>
<Rectangle Width="120" Height="40" Fill="Blue" />
</MenuItem.Header>
</MenuItem>
</ContextMenu>
</Grid.ContextMenu>
</Grid>
</Window>
Code Behind:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
namespace TimersXP
{
public partial class TimersHost : Window
{
public TimersHost()
{
try
{
InitializeComponent();
}
catch (Exception ex)
{
Debug.WriteLine("CTOR Exception: " + ex.Message);
}
// Load the default skin.
Grid mainGrid = this.Content as Grid;
MenuItem item = mainGrid.ContextMenu.Items[0] as MenuItem;
this.ApplySkinFromMenuItem(item);
}
public void OnMenuItemClick(object sender, RoutedEventArgs e)
{
MenuItem item = e.OriginalSource as MenuItem;
// Update the checked state of the menu items.
//Grid mainGrid = this.Content as Grid;
//foreach (MenuItem mi in mainGrid.ContextMenu.Items)
//mi.IsChecked = mi == item;
// Load the selected skin.
this.ApplySkinFromMenuItem(item);
}
void ApplySkinFromMenuItem(MenuItem item)
{
// Get a relative path to the ResourceDictionary which
// contains the selected skin.
string skinDictPath = item.Tag as string;
Uri skinDictUri = new Uri(skinDictPath, UriKind.Relative);
// Tell the Application to load the skin resources.
App app = Application.Current as App;
app.ApplySkin(skinDictUri);
}
}
}
ViewModel:
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
namespace TimersXP
{
public class TimersHostViewModel
{
public TimersHostViewModel()
{
// Load the default skin.
Grid mainGrid = this.Content as Grid; <---- ERROR HERE
}
//public void TimersHostViewModel()
//{
// // Load the default skin.
// Grid mainGrid = TimersHost.Content as Grid;
// MenuItem item = mainGrid.ContextMenu.Items[0] as MenuItem;
// //this.ApplySkinFromMenuItem(item);
//}
public void OnMenuItemClick(object sender, RoutedEventArgs e)
{
MenuItem item = e.OriginalSource as MenuItem;
// Update the checked state of the menu items.
//Grid mainGrid = this.Content as Grid;
//foreach (MenuItem mi in mainGrid.ContextMenu.Items)
// mi.IsChecked = mi == item;
// Load the selected skin.
this.ApplySkinFromMenuItem(item);
}
void ApplySkinFromMenuItem(MenuItem item)
{
// Get a relative path to the ResourceDictionary which contains the selected skin.
string skinDictPath = item.Tag as string;
Uri skinDictUri = new Uri(skinDictPath, UriKind.Relative);
// Tell the Application to load the skin resources.
App app = Application.Current as App;
app.ApplySkin(skinDictUri);
}
}
}
check out this link:
ContextMenu in MVVM
You need to bind your context menu items to a collection/property in your viewmodel. "This." will not work because that is the code behind and does not translate across to a view model.
Put this in view model:
class ContextItem : INotifyPropertyChanged
{
public string Name;
public ICommand Action;
public Brush Icon;
}
ObservableCollection<ContextItem> Items {get;set;}
then in your view's context menu:
<Grid.ContextMenu>
<ContextMenu ItemsSource="{Binding Items}/>
Anything you want to "pass" to the view needs to be a property/collection in your view model, you will never directly use a visual element object like a Gird/Context menu in you viewmodel. WPF handles the binding for you, which is the main benefit of WPF. Just make sure you implement INotifyPropertyChanged for the properties. I didn't to simplify the sample.
Now this does not mean there is never a case for code behind, but it should only involve visual elements, and not the data the visual elements bind to.
Hope this helps
What I am trying to do is to create some sort of "rooms"(like a chat group, a sharing center or whatever you want). All the room are created the same way, but each one of them contains different informations. Each of these rooms is contained in a TabItem. I managed to create dynamically all the Tabitems, to give those a Grid and a Canvas. But at the moment I am facing a problem: I created a ControlTemplate Called RoomMenu that will show different buttons and, the most important, the people connected in this room in a ListBox(I retrieve those people from a WebService each time I change the selected Tabitem). But since my ListBox is in a ControlTemplate I have no idea how to access the ListBox ItemSource to bind a generic List to it. Down Below is the code used to create my rooms and their content.
Here is my room menu class:
public class RoomMenu : ContentControl
{
public RoomMenu()
{
DefaultStyleKey = typeof(RoomMenu);
}
public string Current_room_id;
public string FullName;
public string Rights;
}
And here is the ControlTemplate located in generic.xaml:
<Style TargetType="test:RoomMenu">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="test:RoomMenu">
<Grid x:Name="MenuGrid">
<Border HorizontalAlignment="Stretch" VerticalAlignment="Stretch" BorderBrush="Black" CornerRadius="2" Background="Black">
<StackPanel Orientation="Vertical">
<Border x:Name="Room_friend_border" Background="Gray" CornerRadius="4" Margin="5">
<ListBox x:Name="current_room_friends" ItemsSource="{Binding ''}" Margin="5" Height="230">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding FullName}" Height="20"/>
<TextBlock Text="{Binding Rights}" Height="20"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
<Border x:Name="Room_menu" Background="Gray" CornerRadius="4" Margin="5">
<StackPanel Orientation="Vertical" Margin="10">
<Button Content="Add item" Margin="0,2,0,2"/>
<Button Content="Set changes" Margin="0,2,0,2"/>
<Button Content="Invite friend" Margin="0,2,0,2"/>
<Button Content="Rename room" Margin="0,2,0,2"/>
<Button Content="Delete room" Margin="0,2,0,2"/>
</StackPanel>
</Border>
</StackPanel>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Here is my Dictionnary Class that contains the RoomMenu:
public class Rooms : TabItem
{
public string Room_guid;
public string Room_name;
public string Primary_user_guid;
public string Room_version;
public Grid Room_grid;
public Canvas Room_canvas;
public RoomMenu Room_menu;
}
And this is when I call my ControlTemplate and Add it to my TabItem's Grid:
public void Set_rooms_interface()
{
foreach (KeyValuePair<string, Rooms> kvp in rooms_list)
{
rooms_list[kvp.Key].Room_menu = new RoomMenu();
rooms_list[kvp.Key].Room_canvas = new Canvas();
rooms_list[kvp.Key].Room_grid = new Grid();
//instance grid columns
rooms_list[kvp.Key].Room_grid.ColumnDefinitions.Add(new ColumnDefinition() {Width = new GridLength(900)});
rooms_list[kvp.Key].Room_grid.ColumnDefinitions.Add(new ColumnDefinition());
//Refreshing room canvas
rooms_list[kvp.Key].Room_canvas.Height = rooms_list[kvp.Key].Room_grid.ActualHeight;
rooms_list[kvp.Key].Room_canvas.Width = rooms_list[kvp.Key].Room_grid.ActualWidth;
rooms_list[kvp.Key].Room_canvas = refresh_canvas(kvp.Key);
Grid.SetColumn(rooms_list[kvp.Key].Room_canvas, 0);
Grid.SetColumn(rooms_list[kvp.Key].Room_menu, 1);
//Add Canvas to Grid
rooms_list[kvp.Key].Room_grid.Children.Add(rooms_list[kvp.Key].Room_canvas);
rooms_list[kvp.Key].Room_grid.Children.Add(rooms_list[kvp.Key].Room_menu);
//Setting TabItem Name
rooms_list[kvp.Key].Header = rooms_list[kvp.Key].Room_name;
//Adding Grid to TabItem.Content
rooms_list[kvp.Key].Content = rooms_list[kvp.Key].Room_grid;
//Adding TabItem to TabControl
Room_tab.Items.Add(kvp.Value);
}
}
I'm sorry if the whole question is a bit long but it was the only way to explain clearly what I was trying to do. So if anyone could give me a hint or answer to do some databinding in a ControlTemplate it would greatly help me.
Thank You.
I think you started in the wrong direction when instantiating UI elements in code. The code behind should only contain one line assigning the people list to the current_room_friends DataContext.
Start with simpler examples of binding data to a ListBox like the beautiful planet example of Bea Stollnitz.