Let me state problem first. I would like to implement wrapper around Canvas (let me call it Page) which would implement selecting rectangle around its UIElements which are actually selected.
For this I implemented ISelect interface like so :
interface ISelect {
Point Center {get; set;} //Center of selecting rectangle
Size Dimensions {get; set;} //Dimensions of selecting rectangle
}
Every object that is put to Page implements ISelect interface.
Page has SelectedElements of type ObservableCollection which holds reference to all currently selected elements.
For every entry in SelectedElements i would like to draw rectangle around it.
I have few ideas how to do this :
Every UIElement can implement on its own this rectangle and show it when selected. This option would require for new objects to implement this every time. So I rather not use it.
In Page I could create rectangles in code-behind in add them to the Page. It isn't MVVM recommended priniciple.
In Page XAML create somehind like ItemsControl and bind it to SelectedElements with specific template. This option seems like the best one to me. Please help me in this direction. Should I somehow use ItemsControl?
Thank you.
I don't have time to dig a complete working solution, so this is mostly a collection of suggestions.
Each element should have view model
public abstract class Element: INotifyPropertyChanged
{
bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
OnPropertyChanged();
}
}
}
public class EllipseElement : Element {}
public class RectangleElement : Element {}
Then there are data templates to visualize elements (I can't give you converter code, but you can replace it with another, look here).
<DataTemplate DataType="{x:Type local:EllipseElement}">
<Border Visibility="{Binding IsSelected, Converter={local:FalseToHiddenConverter}}">
<Ellipse ... />
</Border>
</DataTemplate>
<DataTemplate DataType="{x:Type local:RectangleElement}">
<Border Visibility="{Binding IsSelected, Converter={local:FalseToHiddenConverter}}">
<Rectangle ... />
</Border>
</DataTemplate>
Then bind ObservableCollection of elements to canvas (which is tricky, see this answer, where ItemsControl is used to support binding).
Your selection routine has to hit-test elements and set/reset their IsSelected property, which will show border. See here regarding how to draw over-all selection rectangle.
Related
I'm trying to improve performance with my WPF application and I'm having problems with a complex ItemsControl. Although I've added Virtualization, there's still a performance problem and I think I've worked out why.
Each item contains a series of expandable areas. So the user sees a summary at the start but can drill down by expanding to see more information. Here's how it looks:
As you can see, there's some nested ItemsControls. So each of the top level items has a bunch of Hidden controls. The virtualization prevents off-screen items from loading, but not the hidden items within the items themselves. As a result, the relatively simple initial layout takes a significant time. Flicking around some of these views, 87% of time is spent parsing and Layout, and it takes a few seconds to load.
I'd much rather have it take 200ms to expand when (if!) the user decides to, rather than 2s to load the page as a whole.
Asking for advice really. I can't think of a nice way of adding the controls using MVVM however. Is there any expander, or visibility based virtualization supported in WPF or would I be creating my own implementation?
The 87% figure comes from the diagnostics:
If you simply have
- Expander
Container
some bindings
- Expander
Container
some bindings
+ Expander
+ Expander
... invisible items
Then yes, Container and all bindings are initialized at the moment when view is displayed (and ItemsControl creates ContentPresenter for visible items).
If you want to virtualize content of Expander when it's collapsed, then you can use data-templating
public ObservableCollection<Item> Items = ... // bind ItemsControl.ItemsSource to this
class Item : INotifyPropertyChanged
{
bool _isExpanded;
public bool IsExpanded // bind Expander.IsExpanded to this
{
get { return _isExpanded; }
set
{
Data = value ? new SubItem(this) : null;
OnPropertyChanged(nameof(Data));
}
}
public object Data {get; private set;} // bind item Content to this
}
public SubItem: INotifyPropertyChanged { ... }
I hope there is no need to explain how to to do data-templating of SubItem in xaml.
If you do that then initially Data == null and nothing except Expander is loaded. As soon as it's expanded (by user or programmatically) view will create visuals.
I thought I'd put the details of the solution, which is pretty much a direct implementation of Sinatr's answer.
I used a content control, with a very simple data template selector. The template selector simply checks if the content item is null, and chooses between two data templates:
public class VirtualizationNullTemplateSelector : DataTemplateSelector
{
public DataTemplate NullTemplate { get; set; }
public DataTemplate Template { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (item == null)
{
return NullTemplate;
}
else
{
return Template;
}
}
}
The reason for this is that the ContentControl I used still lays out the data template even if the content is null. So I set these two templates in the xaml:
<ContentControl Content="{Binding VirtualizedViewModel}" Grid.Row="1" Grid.ColumnSpan="2" ><!--Visibility="{Binding Expanded}"-->
<ContentControl.Resources>
<DataTemplate x:Key="Template">
<StackPanel>
...complex layout that isn't often seen...
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="NullTemplate"/>
</ContentControl.Resources>
<ContentControl.ContentTemplateSelector>
<Helpers:VirtualizationNullTemplateSelector Template="{StaticResource Template}" NullTemplate="{StaticResource NullTemplate}"/>
</ContentControl.ContentTemplateSelector>
</ContentControl>
Finally, rather than using a whole new class for a sub-item, it's pretty simple to create a "VirtualizedViewModel" object in your view model that references "this":
private bool expanded;
public bool Expanded
{
get { return expanded; }
set
{
if (expanded != value)
{
expanded = value;
NotifyOfPropertyChange(() => VirtualizedViewModel);
NotifyOfPropertyChange(() => Expanded);
}
}
}
public MyViewModel VirtualizedViewModel
{
get
{
if (Expanded)
{
return this;
}
else
{
return null;
}
}
}
I've reduced the 2-3s loading time by about by about 75% and it seems much more reasonable now.
This simple solution helped me:
<Expander x:Name="exp1">
<Expander.Header>
...
</Expander.Header>
<StackPanel
Margin="10,0,0,0"
Visibility="{Binding ElementName=exp1, Path=IsExpanded, Converter={StaticResource BooleanToVisibilityConverter}}">
<Expander x:Name="exp2">
<Expander.Header>
...
</Expander.Header>
<StackPanel
Margin="10,0,0,0"
Visibility="{Binding ElementName=exp2, Path=IsExpanded, Converter={StaticResource BooleanToVisibilityConverter}}">
An easier way to achieve this is to change the default Visibility of the contents to Collapsed. In this case WPF won't create it initially, but only when a Trigger sets it to Visible:
<Trigger Property="IsExpanded" Value="true">
<Setter Property="Visibility"Value="Visible" TargetName="ExpandSite"/>
</Trigger>
Here "ExpandSite" is the ContentPresenter within the default ControlTemplate of the Expander control.
Note that this has been fixed in .NET - see the default style from the WPF sources on github.
In case you have an older version, you can still use this fixed control template to update the old one with an implicit style.
You can apply the same technique to any other panel or control.
It's easy to check if the control was already created with Snoop. Once you attached it to your application, you can filter the visual tree with the textbox on the top left. If you don't find one control in the tree, it means it was not created yet.
I customized a Listbox to show a Pie-Chart (each Listitem is one slice of the Pie). To do this i used an Itemtemplate which (for now) only consists of a Shape. To make those shapes form a full circle, i calculated start/endangle for each piece and used a custom ItemsPanelTemplate to stack the Items on top of each other.
When I click anywhere near the Pie, the "last" item gets selected since it is located on top of the others. This is quite obvious, but I hoped since the ItemTemplate only contains a Shape, it would inherit the hit-testing from there and not assume that all items are represented by rectangles.
Where am I supposed to include the hittesting? I would like to set IsHitTestVisible="false" to everything inside my ItemTemplate, except for the shape - but since it doesn't actually contain anything except for my shape, i am stuck right now.
Edit:
I tried surrounding my Shape with a Grid with transparent background, on which i did set IsHitTestVisible="false". This still results in selecting the last element on each click while i would've assumed that nothing would be selected. I think i might be confused about how hittesting is supposed to work?
Code:
Since i am new to WPF i might have missed something during the implementation. I added the relevant codeparts of a minimal example:
My Shape-class:
public class PiePiece : Shape
{
protected override Geometry DefiningGeometry
{
get { return GetPieGeometry() }
}
//some DependencyProperties and helper methods.
private Geometry GetPieGeometry()
{
//calculations
}
}
XAML:
<Window [...] xmlns:Runner="clr-namespace:MyNamespace">
<Window.Resources>
<!-- some custom converters -->
<!-- ListBox-Style with the custom ControlTemplate for my Listbox -->
<ItemsPanelTemplate x:Key="ItemPanel">
<Grid/>
</ItemsPanelTemplate>
</Window.Resources>
<ListBox [...] ItemsPanel="{StaticResource ItemPanel}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Background="{x:Null}" IsHitTestVisible="False">
<Runner:PiePiece IsHitTestVisible="False" [...]>
<!-- Dependency Properties are set via Multibinding here -->
</Runner:PiePiece>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Window>
I finally found the reason why the hittesting did not work as desired:
The default template for the ListBoxItem-Style surrounds the ContentPresenter with a Border with transparent background. All click-events were caught and handled by that border, instead of my ContentPresenter. Writing my own style for the ListBoxItem and setting the Background to {x:null} as suggested by Gaz fixed the problem (as did removing the border, but I added another one by now for further customizations).
I have been developing my first MVVM WPF application, in which I want to draw a graph containing nodes and edges. Currently I am doing all drawing logic in the code behind of my view, iterating over the nodes, creating shapes accordingly and adding them to a canvas.
Because I do not want to keep track of the shapes, and just want them to be drawn based on the data that is given (i.e. the nodes) I have decided to create an ObservableCollection of both the Nodes and Edges, and bind an ItemsControl to these in order to automatically draw the shapes.
For now I am focussing on drawing the nodes, and came up with the following XAML code:
<ItemsControl x:Name="ItemsControlCanvas" ItemsSource="{Binding Path=Nodes}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas>
<Canvas.LayoutTransform>
<ScaleTransform ScaleX="{Binding ?}" ScaleY="{Binding ?}"/>
</Canvas.LayoutTransform>
</Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Fill="Blue" Width="4" Height="4" />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Top" Value="{Binding Path=Position.Y}" />
<Setter Property="Canvas.Left" Value="{Binding Path=Position.X}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
In my view, I have two properties ScaleX and ScaleY which I set when the user scrolls on the canvas. In my earlier code behind version, I would create a ScaleTransform with those properties, and apply it to the named canvas using a LayoutTransform.
public partial class GraphOverview : Page
{
public double ScaleX { get; set; }
public double ScaleY { get; set; }
The problem in this version is that I cannot get around to get it working in my new XAML only code. I would like for the ScaleX and ScaleY attributes of the ScaleTransform in the ItemsPanelTemplate to be bound to the properties in my view. Only, normal ElementName binding does not work for some reason. More clearly, the canvas is presumably not aware of the view, I assume because it is a template. Moreover, I cannot call the canvas from the code behind, even when it has a name.
I have tried several solutions, fumbling with RelativeResources and the like, but I think I do not clearly understand why a ItemsPanelTemplate is so disconnected from everything else in the XAML code. Thanks in advance!
Update
Maybe I should clarify that this could be easily resolved by moving the ScaleX and ScaleY properties to the ViewModel, and bind to those properties. But in my opinion such View-specific properties should not reside in the ViewModel.
You are correct when you say view specific information should not be in the view model. But in your case, the positions becomes part of a model because the positions are nothing but data based on which your view should behave. In such cases you can consider them as part of model rather than view. Write a separate Model Class and use it in the observable collection of your ViewModel Class. I hope it clears your doubt.
public class Node
{
public double ScaleX { get; set; }
public double ScaleY { get; set; }
}
Edit
Answering your questions. -
But the Scale properties specify how the Canvas should behave, so does the View not already serve as a 'model' for this? It seems superfluous to me to create a new Model just to set the scale properties of a single Canvas? The view should be merely concerned with drawing data, and since the properties serve as modifications of these drawings, they belong in the View, don't they?
You want to draw the graph based on the data provided. The Node class is going to provide the data for you. It stores the data and not the logic how the canvas should render, the XAML code uses this data and has the logic to show it in the view.
so does the View not already serve as a 'model' for this?
The implementation you have tried has the model in the view, the idea is to separate the model from the view. It might look superfluous initially to create a class just to hold 2 double values. But there are some advantages if you abstract them away from the view.
In the view model you can create a observable collection for this Node class(Model). You cannot access it from Viewmodel if you have the ScaleX an ScaleY in View.
In future you might want to change ScaleX and ScaleY to be dervied into different scale e.g. Logarmic scale/ Different unit. In such cases you will have to change the logic in ViewModel to do so and never have to worry about changing the View. But if you have this Observable collection in the view, you have to change the view for making a change to the data/model.
Lastly - you can write unit tests for whatever you have in the ViewModel but not the View.
Normally ScaleX and ScaleY will be part of the view, but in your case they change and stores data. Hence you need to abstract this ScaleX and ScaleY into a different layer for preserving the MVVM concept.
I'm not sure if you have set the DataContext of your view, it would help to show more of your code behind
internal ViewModel viewModel { get; set; }
public View()
{
DataContext = (viewModel = new ViewModel());
}
Are you binding the ObservableCollection to ItemSource of the nodes? I'm not familiar with drawing, but when I've bound to children, I've never achieved results.
But I've only developed about 4 projects utilizing MVVM.
I'm using several lists across my project instead of trees - for proper virtualization (a lot of items in tree structure).
Those lists are pretty much the same. The only difference is in DataTemplates. Those lists have a few events bound, which I have to copy & update in several places. Current events are used to:
prevent horizontal auto-scrolling
support for arrow keys to navigate through tree structure
I found no way to bind events in a single style in resource dictionary, as events must belong to specific class. So I have to copy exactly same events between classes and bind them to specific lists. That is quite a lot of text, both in XAML and code.
What I wanted to do is to define a new user control, deriving EVERYTHING from standart ListBox, but overriding a few minor methods (instead of events). And reuse this control everywhere where I need such a list without having to copy all the events.
Problem is - it requires me to define custom <UserControl ... />. Is there a way to just use ListBox template/style there? I need no GUI modifications from standart ListBox.
I could be missing some simple way to perform what I want. I'd appreciate any way to do this.
Not sure about your setup but you will probably have to override the ListBox and ListBoxItem. Then override some methods :
public partial class MyListBox: ListBox
{
protected override System.Windows.DependencyObject GetContainerForItemOverride()
{
return new MyListBoxItem();
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is MyListBoxItem;
}
}
public class MyListBoxItem : ListBoxItem
{
}
This will force your containers to be ListBoxItem overrides.
Now you just have to implement yous specific code tof keys in ListBoxItem overrides. If you don't need any style changes the default ListBox style will be applied.
Now you can use it in your XAML:
<local:MyListBox ItemsSource="{Binding Items}">
<local:MyListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding id}"/>
</DataTemplate>
</local:MyListBox.ItemTemplate>
</local:MyListBox>
Hi,
I have a requirement to show receipt preview as part of WPF page. Sample of receipt is attached.
Each line of text on the receipt can have different alignment(some center, some right or left) and color depending on configuration. Also, the number of lines can vary for each receipt type. I am wondering which controls to be used to effectively implement this. I can create labels dynamically in code behind depending on number of lines and align each one differently with different foreground color but just looking for an effective way if there is any. The width of receipt does NOT vary but length may. Font is same for all lines and all receipt types. Any ideas are really appreciated.
Thanks
It is normally better to avoid dynamically adding controls like labels or textblocks from your code behind. This type of code is difficult to read and almost impossible to test. Instead, you should use a view-model class (look up the MVVM pattern). Your view-model could have a property returning a list of ReceiptItem and then in your view (the XAML file) you make an ItemsControl and bind it to your list of ReceiptItems. Now you can create a template for the ReceiptItem class so that they show up a desired using Label, TextBlock, or whatever you decide is appropriate.
For example, in C# you would need two classes:
public class MyReceiptViewModel
{
public List<ReceiptItem> ReceiptItems { get; set; }
}
public class ReceiptItem
{
public string Content { get; set; }
public bool IsHighlighted { get; set; }
}
Your view might look like (this assumes that you have an instance of MyReceiptViewModel as your data context):
<ItemsControl ItemsSource="{Binding ReceiptItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock
Text="{Binding Content}"
Foreground="{Binding IsHighlighted, Converter={StaticResource MyColorFromBooleanConverter}}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>