Imagine a form designer with a grid overlay that would represent coordinates on a plane. I'm trying to bind the properties of the grid overlay to the Canvas within a custom ItemsControl.
The grid is created using a VisualBrush. The VisualBrush's Viewbox and Viewport are bound to a Rect in the code, as well are the Height and Width of the Rectangle used to display the grid tile. However, when the control displays, the grid tiles seem to be "infinitely small" (the grid is just grey) in that if I zoom into the grid, the program will eventually just seize up, unable to render it. Obviously, this is not the effect I'm going for.
<Style TargetType="{x:Type Controls:FormControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Controls:FormControl}">
<Border Background="White"
BorderBrush="Black"
BorderThickness="1"
Padding="49">
<Grid Height="{TemplateBinding CanvasHeight}"
Width="{TemplateBinding CanvasWidth}">
<Grid.Background>
<!--"0,0,6,11.3266666666667"-->
<VisualBrush TileMode="Tile"
Viewbox="{TemplateBinding GridUnitViewbox}"
ViewboxUnits="Absolute"
Viewport="{TemplateBinding GridUnitViewbox}"
ViewportUnits="Absolute">
<VisualBrush.Visual>
<Rectangle Height="{Binding RelativeSource={RelativeSource TemplatedParent},Path=GridUnitViewbox.Height}"
HorizontalAlignment="Left"
Opacity="{TemplateBinding GridOpacity}"
Stroke="Black"
StrokeThickness=".1"
VerticalAlignment="Top"
Width="{Binding RelativeSource={RelativeSource TemplatedParent},Path=GridUnitViewbox.Width}" />
</VisualBrush.Visual>
</VisualBrush>
</Grid.Background>
<ItemsPresenter />
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="Controls:FormControlItem">
<Setter Property="Canvas.Left"
Value="{Binding Path=X}" />
<Setter Property="Canvas.Top"
Value="{Binding Path=Y}" />
</Style>
</Setter.Value>
</Setter>
</Style>
Any idea what I am doing wrong here. Thanks.
EDIT:
Maybe a little more background of what I'm doing may put it in better context. I work for a tax preparation software company and, currently, our forms division creates substitute forms using a markup that was written for our product like a million years ago. It's a bit cumbersome creating forms this way, so I'm developing a visual "Form Designer" for them that will be more like a WYSIWYG and translate the contents of the designer into markup. Well, the IRS is real anal about everything on the form being EXACTLY where it was on the original, so there is a very loose standard by which a "grid overlay" can be placed over the form to determine where things need to go; basically a coordinate plane of sorts.
FormControl is essentially the visual representation of one of these substitute forms that one of the forms designers would be creating.
CanvasWidth and CanvasHeight are CLR wrappers to Dependency Properties. They are assigned values in OnApplyTemplate() by multiplying the dimensions of GridUnitViewbox by however many grid tiles need to be in the grid overlay, ie. a 78x63 grid in most cases.
The names CanvasWidth and CanvasHeight I think might be a little misleading in that they do not refer to the Canvas control, but to the Grid that houses the Canvas (probably need to change the naming convention). That said, CanvasHeight, CanvasWidth and GridUnitViewbox are not dependent on any control's properties, but rather calculations that are done in OnApplyTemplate().
public static readonly DependencyProperty GridUnitViewboxProperty =
DependencyProperty.Register("GridUnitViewbox", typeof(Rect), typeof(FormControl),
new FrameworkPropertyMetadata(new Rect(0, 0, 6, 11.3266666666667),
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsParentMeasure));
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
FormattedText formattedText = new FormattedText(
"X",
CultureInfo.GetCultureInfo("en-us"),
FlowDirection.LeftToRight,
new Typeface("Courier New"),
10,
Brushes.Black);
this.GridUnitViewbox = new Rect(0, 0, formattedText.WidthIncludingTrailingWhitespace, formattedText.Height);
if (this.PageLayout == PageLayoutType.Landscape)
{
this.CanvasHeight = this.GridUnitViewbox.Height * 48.0;
this.CanvasWidth = this.GridUnitViewbox.Width * 109.0;
}
if (this.PageLayout == PageLayoutType.Portrait)
{
this.CanvasHeight = this.GridUnitViewbox.Height * 63.0;
this.CanvasWidth = this.GridUnitViewbox.Width * 78.0;
}
}
The Grid control is actually doing exactly what I want it to do. It is the VisualBrush and the Rectangle within that are either not binding correctly or are not being updated properly. The comment right below <Grid.Background> was the hard-coded testing value that I was using for VisualBrush's Viewbox and Viewport as well as Rectangle's dimensions before I got around to binding the values and it produced, visually, exactly what I was going for. I've also confirmed that 6 and 11.3266666666667 are, in fact, the values for the GridUnitViewbox's dimensions during runtime.
I have a feeling that the binding is producing '0,0,0,0' however, because the grid overlay is just a grey shading that is eating up an immense amount of resources; locks the program, in fact, if you zoom into it. As you can see, in the code I tried adding AffectsMeasure and AffectsParentMeasure to the Metadata options of the dependency property in hopes that perhaps the UI was not updating properly after GridUnitViewbox's dimensions were updated, but I was wrong. I'm not sure what else it could be.
Alright, just in case anyone else encounters a similar issue, I found the workaround. Apparently VisualBrush is a little finicky about TemplateBindings. I adjusted the XAML thusly and it solved the problem:
<VisualBrush TileMode="Tile"
Viewbox="{Binding RelativeSource={RelativeSource TemplatedParent},Path=GridUnitViewbox}"
ViewboxUnits="Absolute"
Viewport="{Binding RelativeSource={RelativeSource TemplatedParent},Path=GridUnitViewbox}"
ViewportUnits="Absolute">
Here is the article where I got the information from.
What is CanvasWidth and CanvasHeight? Are they defined?
Also, I believe that a canvas has no size unless explicitly specified. When you bind to it, the width/height may be zero.
Try ActualWidth and ActualHeight or the canvas after the rendering pass to see if they contain any values, but afaik they don't unless you provide one.
Related
I'm trying to render a row of interlocking arrows, like this:
Each arrow represents a step in a workflow, and the color of each arrow is determined by its step's position in the workflow. While the example above has 3 colors (before, current, after), so far I've implemented only 2 colors (before/current, after) and that's fine for the purposes of this question. I've got an IMultiValueConverter to handle those colors.
The workflow steps are represented as a StepStruct which has a Step property (set manually, more on this later), and a VmFunc property which returns the view model for that step. My current IMultiValueConverter uses the index of the step in the list of steps, rather than the actual Step value.
My problem is the arrows. Initially I have each step as a canvas rendering a simple rectangle, and that's easy to get working. But to make the arrow, I've used a PolyLine that I want to position at the right of the canvas, indeed starting at the right of one canvas and overflowing into the next.
I can't get the Panel.ZIndex to work in such a way that a canvas's arrow is visible overflowing into its right-hand neighbor.
My code is pasted below. Of note:
I've bound the Polyline's Panel.Zindex (which I've read affects its parent canvas's z-index) to the StepStruct's Step property, and I've set the Step values so that each step's value is less than the value of the step to its left, which should show left steps over the right step.
I've commented a line setting the Canvas.Left property of each PolyLine. When I uncomment this line (and move it into place), the Polyline indeed moves over to the right side of the canvas, but it's invisible, which I imagine is because it's hidden behind the canvas to its right. (I've confirmed this by changing the PolyLine to start with negative x-values, so you can see the part that's not blocked by the neighboring canvas. This is not pictured.)
I've rendered each canvas's Step as text, and those TextBlocks all have an identical z-index (40) so that they're in front of some PolyLines and behind others, which shows that the PolyLine does have its z-index set right, at least within its own canvas.
<Border Grid.Row="0" BorderBrush="{StaticResource BackgroundBlack}" BorderThickness="2">
<!--Progress bar-->
<ItemsControl ItemsSource="{Binding StepViewModels}" Grid.Row="0" x:Name="progressBar">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="1"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas Height="24">
<Canvas.Style>
<Style TargetType="Canvas">
<Setter Property="Background" Value="{StaticResource BackgroundDarkGrey}"/>
<Style.Triggers>
<DataTrigger>
<DataTrigger.Binding>
<!-- the binding. this part works so I'm omitting the code -->
</DataTrigger.Binding>
<DataTrigger.Value>True</DataTrigger.Value>
<Setter Property="Background" Value="{StaticResource DisabledGrey}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Canvas.Style>
<Polyline Points="0,-1 20,11 0,25" Stroke="{StaticResource BackgroundBlack}" StrokeThickness="2"
Fill="{Binding Background, RelativeSource={RelativeSource AncestorType=Canvas}}"
Panel.ZIndex="{Binding Step}"
/>
<!--Canvas.Left="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType=Canvas}}"-->
<TextBlock Text="{Binding Step}" FontWeight="ExtraBlack" Panel.ZIndex="40" Foreground="Pink" FontSize="20" Canvas.Left="8"/>
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
The result (with the Canvas.Left still commented)
How do I get the arrows to render at the end of one canvas and atop the next, as desired?
(I could of course keep it this way and make it work but the bindings would be very annoying and complicated, needing to bind each arrow to the color of the previous canvas; or rather do some kind of conversion involving IndexOf - 1)
For ZIndex:
Each item is wrapped by a ContentPresenter, this is done by the ItemContainerGenerator inside this ItemsControl instance.
So you need to edit the container to get a new ZIndex(via a ContentPresenter style and a binding for example).
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Panel.ZIndex" Value="{Binding XXX}" />
</Style>
</ItemsControl.ItemContainerStyle>
For the shape:
I would recommend using a Path control with a predefined shape geometry like:
"M 0,0 L30,0 L34,5 L30,10 L0,10 z"
and have a negative margin at the end like Margin="0,0,-4,0"
I'm using an MVVM-oriented fork of the GreatMaps.NET library (https://greatmaps.codeplex.com/), but my question comes down to a basic issue with using a custom Canvas as my ItemsPanel for an ItemsControl. Essentially, the map is an ItemsControl, with a MapCanvas placing its children as follows:
protected override Size ArrangeOverride(Size arrangeSize)
{
foreach (UIElement child in Children)
{
PointLatLng position = new PointLatLng(GetTop(child), GetLeft(child));
GMapControl map = Owner as GMapControl;
if (map != null)
{
GPoint p = map.FromLatLngToLocal(position);
p.Offset(-(long)(map.MapTranslateTransform.X + child.DesiredSize.Width * 0.5), -(long)(map.MapTranslateTransform.Y + child.DesiredSize.Height * 0.5));
Rect rect = new Rect(p.X, p.Y, child.DesiredSize.Width, child.DesiredSize.Height);
child.Arrange(rect);
}
}
return arrangeSize;
}
This works because my ItemContainerStyle binds to Latitude and Longitude in the ViewModel for each map item as follows (along with ZIndex, which I have set to 99 as a placeholder):
<Setter Property="Canvas.Left" Value="{Binding Longitude}" />
<Setter Property="Canvas.Top" Value="{Binding Latitude}" />
<Setter Property="Canvas.ZIndex" Value="{Binding ZIndex}" />
I'm using DataTemplates with DataType fields to adjust how each element gets displayed, and that part is working properly.
There are elements (such as lines for routes on the map) that I need to have not as point-elements but as sequences of subelements. To do this, I just followed the pattern, and made a DataTemplate where I use another MapCanvas bound to the same MapControl as its Owner (here, MapOverlay is just a copy of MapCanvas that overrides OnRender to draw lines between its children):
<DataTemplate DataType="{x:Type viewModels:RouteViewModel}">
<ItemsControl ItemsSource="{Binding Locations}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<wpf:MapOverlay Name="MapOverlay" Owner="{Binding Path=., RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type wpf:GMapControl}}}"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Left" Value="{Binding Longitude}" />
<Setter Property="Canvas.Top" Value="{Binding Latitude}" />
<Setter Property="Canvas.ZIndex" Value="{Binding ZIndex}" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Fill="Blue" Stroke="Black" Width="10" Height="10"></Ellipse>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
To keep the bindings working, the RouteViewModel still has its own Lat/Lng, which seem to only work when I position them at the initial point of the list of points along the route:
public double Latitude => Locations.Select(loc => loc.Latitude).FirstOrDefault();
public double Longitude => Locations.Select(loc => loc.Longitude).FirstOrDefault();
In Summary:
I have a GMapControl ItemsControl using a MapCanvas to geospatially position elements. DataTypes are used to select which DataTemplate gets used for each element. This works for point-based elements such as markers.
One of the DataTemplates is an ItemsControl using yet another MapCanvas variant (MapOverlay) as its ItemsPanel to draw lines between subelements. This works at launch, when its ArrangeOverride executes.
Panning the map works, though ArrangeOverride does not get called in the MapOverlay.
The problem: while my elements appear initially correctly positioned, zooming the map (which is triggering an InvalidateVisual and UpdateLayout via the map control) does not cause ArrangeOverride to be called on the nested MapOverlay. This causes the route to no longer be positioned properly -- it does not scale.
Any suggestions as to why the arrange invalidation is not trickling down to the nested MapOverlay and/or tips on how to fix it?
Addendum: The route was only placed correctly when the map was initially positioned at its first element -- the Latitude/Longitude of the route as a whole is a separate issue that I would welcome ideas on.
Not actually an answer to your question, but it may give you a general idea how to show a route, i.e. a collection of circles connected by lines.
The following sample use the XAML MapControl library, but may well be implemented based on any other map library that has some kind of MapItemsControl (e.g. MS Bing Maps).
The idea is to have a collection of Location instances that make up your route. You would have a MapPolyline and bind its Locations property to the route points collection. Then put a MapItemsControl on top of it and bind its ItemsSource to the same collection. Note that the ItemTemplate uses Path controls with EllipseGeometries instead of Ellipses, because they are centered at the point location, while Ellipses are top/left aligned.
<map:MapPanel>
<map:MapPolyline Locations="{Binding RouteLocations}"
Stroke="DarkBlue" StrokeThickness="3"/>
<map:MapItemsControl ItemsSource="{Binding RouteLocations}">
<map:MapItemsControl.ItemContainerStyle>
<Style TargetType="map:MapItem">
<Setter Property="map:MapPanel.Location" Value="{Binding}"/>
</Style>
</map:MapItemsControl.ItemContainerStyle>
<map:MapItemsControl.ItemTemplate>
<DataTemplate>
<Path Fill="Red">
<Path.Data>
<EllipseGeometry RadiusX="5" RadiusY="5"/>
</Path.Data>
</Path>
</DataTemplate>
</map:MapItemsControl.ItemTemplate>
</map:MapItemsControl>
</map:MapPanel>
If the route points aren't Locations you could use binding converters in both bindings to convert from your route point type to Location.
I am trying to print a chart generated during a report. I am able to put the chart on a DocumentPaginator document, but I am having trouble resizing the chart to fit the page. I noticed that if I changed the size of the reporting program which would change the Charts size would affect the scaling of the Chart. This realization showed me that the Chart's ActualWidth and ActualHeight were directly linked to the scaling.
I tried:
myChart.Width = newWidth;
myChart.Height = newHeight;
Measure(myChart.RenderSize);
Arrange(new Rect(0, 0, newWidth, newHeight));
But this caused my visual Chart in the reporting program to resize and the printable chart wouldn't resize to the new size until the second print.
Realizing that myChart was connected to reportChart I tried to copy/clone reportChart to myChart.
I tried:
public class Copy<T>
{
public static T DeepCopy<T>(T element)
{
string xaml = XamlWriter.Save(element);
StringReader xamlString = new StringReader(xaml);
XmlTextReader xmlTextReader = new XmlTextReader(xamlString);
var DeepCopyobject = (T)XamlReader.Load(xmlTextReader);
return DeepCopyobject;
}
}
or
myChart = XamlReader.Parse(XamlWriter.Save(reportChart.DataContext)) as Chart
but string xaml = XamlWriter.Save(element); would take too long and both would cause a stackoverflow.
I am using
myChart = new Chart() { DataContext = reportChart.DataContext }
to make my copy, but ActualWidth and ActualHeight come across '0' so I can't tell if the Chart's DataContext copied correctly.
I did get my Chart to resize using
myChart.Width = newWidth;
myChart.Height = newHeight;
myChart.Measure(new System.Windows.Size(newWidth, newHeight));
myChart.Arrange(new Rect(0, 0, newWidth, newHeight));
or to say my Chart's ActualWidth and ActualHeight to update to the size I want, but I am getting a black image on my document where the chart should be.
So how do I print a chart with it properly scaled to a selected paper size?
So I found something that works for me. I don't feel it is the cleanest way to do it.
Since I need to scale the Chart I am trying to print I need to copy/clone the Chart.
I used myNewChart = new Chart() { DataContext = myOldChart.DataContext } like stated before.
I added a new window to my project, and rendered my new chart in there so I don't get a black image from it.
ConvertingWindow.xaml Code. This code matches my orignal Chart's code.
<Window x:Class="QScanReportPrinting.ConvertingWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="ConvertingWindow"
xmlns:cht="clr-namespace:System.Windows.Controls.DataVisualization.Charting;assembly=System.Windows.Controls.DataVisualization.Toolkit">
<Grid>
<Grid.Resources>
<!-- CutomColumnStyle Style -->
<Style x:Key="CutomColumnStyle" TargetType="cht:ColumnDataPoint">
<!--Background Color-->
<Setter Property="Background" Value="{Binding barColor}"/>
<!--Annotations, or column value labels-->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="cht:ColumnDataPoint">
<Grid>
<Rectangle Fill="{TemplateBinding Background}" Stroke="Black"/>
<Grid Margin="0 -20 0 0" HorizontalAlignment="Center" VerticalAlignment="Top">
<TextBlock Text="{TemplateBinding FormattedDependentValue}" FontWeight="Bold" Margin="2">
<TextBlock.RenderTransform>
<RotateTransform Angle="-60" />
</TextBlock.RenderTransform>
</TextBlock>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Grid.Resources>
<!--Chart for Graph-->
<cht:Chart x:Name="UI_Chart" Title="{Binding GraphTitle}" Background="White">
<cht:Chart.Series>
<cht:ColumnSeries Title="{Binding ChartKey}" ItemsSource="{Binding GraphDataCollection}" IndependentValueBinding="{Binding Path=X}" DependentValueBinding="{Binding Path=Y}"
DataPointStyle="{StaticResource CutomColumnStyle}">
<cht:ColumnSeries.IndependentAxis>
<cht:CategoryAxis Orientation="X">
<cht:CategoryAxis.AxisLabelStyle>
<Style TargetType="cht:AxisLabel">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="cht:AxisLabel">
<TextBlock Text="{TemplateBinding FormattedContent}">
<TextBlock.LayoutTransform>
<RotateTransform Angle="-60"/>
</TextBlock.LayoutTransform>
</TextBlock>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</cht:CategoryAxis.AxisLabelStyle>
</cht:CategoryAxis>
</cht:ColumnSeries.IndependentAxis>
</cht:ColumnSeries>
</cht:Chart.Series>
</cht:Chart>
</Grid>
Then in my VM I call.
private void GetChartVisual()
{
// Initialize variable
cw = new ConvertingWindow();
cw.UI_Chart.DataContext = myNewChart.DataContext;
// Set MainWindow to be the owner of this window
cw.Owner = Application.Current.MainWindow;
// Set DataContext to this DataContext
// Allows binding with variables already loaded
cw.DataContext = this;
cw.Show();
myNewChart = cw.UI_Chart;
cw.Close();
}
By doing this it renders my visual. I then can resize it to what I want and not affect my original Chart. Not the prettiest thing, but it works.
Need your help. I have a ListBox (with virtualization) which displays a ScrollViewer.
My ListBox items are expandable, and while expanded their hight may exceed the visible scrolling area.
The problem i'm expiriencing is that when the list box item is exceeds the visible scrolling area - scrolling jumps to the next ListBox item rather than simply scrolling the view.
Check this code:
<ListBox Grid.Row="1" Grid.Column="0" DataContext="{Binding SpecPackageSpecGroupListViewModel}" VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.VirtualizationMode="Recycling"
ItemContainerStyle="{StaticResource SpecPackageSpecGroupListBoxStyle}" ScrollViewer.IsDeferredScrollingEnabled="True"
ItemsSource="{Binding SortedChildren}" ScrollViewer.CanContentScroll="True"
Background="Transparent"
BorderThickness="0" SelectionMode="Extended"
Margin="5,5,5,5">
<ListBox.ItemTemplate>
<DataTemplate>
<Controls:SpecPackageSpecGroupControl/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Of-course, i can't wrap my ListBox with another scroller since it will turn off the visualization (which is very impotent to me).
If i set CanContentScroll to False everything works as expected - but the virtualization stops working.
HELP!!!
Gili
Ok, so just before i was about to give up and somehow learn how to live with this bug i bumped into a post (which i can't seem to find now) that suggests that TreeView does support Pixel-Based scrolling (AKA Physical Scrolling) without turning off visualization.
So i tried this and indeed - it works! Made sure to verify that virtualization works, tested with ~1000 items and also set a break point on my control constructor and made sure it is called when my view is scrolled.
The only disadvantage of using TreeView instead of ListBox is that TreeView doesn't seem to support multiple item selection (which i needed) - but implementing this is way much easier than implementing the smart scrolling for ListBox.
I created a style for TreeViewItem that makes the TreeViewItem look and behave just like ListBoxItem, this is really not mandatory - but i preferred it like this (beside the fact that the basic style has stretching issues which i had to fix with styling). Basically i removed the ItemsPresenter and stayed only with the ContentPresenter since my data is not hierarchical:
<Style x:Key="MyTreeViewItemStyle" TargetType="{x:Type TreeViewItem}">
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="SnapsToDevicePixels" Value="true"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
<Setter Property="OverridesDefaultStyle" Value="true"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeViewItem}">
<Border Name="myBorder"
SnapsToDevicePixels="true"
CornerRadius="0,0,0,0"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
BorderThickness="0"
BorderBrush="Transparent"
Height="Auto"
Margin="1,1,1,3"
Background="Transparent">
<ContentPresenter Grid.Column="1" x:Name="PART_Header" HorizontalAlignment="Stretch" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" ContentSource="Header"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Now - the only thing i've got left to do is implement the multi-selection tree view.
There might be different approaches to implement such behavior, i took the ViewModel approach.
Derived from TreeView i created a new MultiSelectionTreeView:
public class MultiSelectionTreeView : TreeView
{
private static bool CtrlPressed
{
get
{
return Keyboard.IsKeyDown(Key.LeftCtrl);
}
}
protected override void OnSelectedItemChanged(RoutedPropertyChangedEventArgs<object> e)
{
base.OnSelectedItemChanged(e);
var previouseItemViewModel = e.OldValue as IMultiSelectionTreeViewItemViewModel;
if (previouseItemViewModel != null)
{
if (!CtrlPressed)
previouseItemViewModel.IsSelected = false;
}
var newItemViewModel = e.NewValue as IMultiSelectionTreeViewItemViewModel;
if (newItemViewModel != null)
{
if (!CtrlPressed)
newItemViewModel.ClearSelectedSiblings();
newItemViewModel.IsSelected = true;
}
}
}
Where IMultiSelectionTreeViewItemViewModel is as follows:
public interface IMultiSelectionTreeViewItemViewModel
{
bool IsSelected { get; set; }
void ClearSelectedSiblings();
}
Of course - now it is my responsibility to handle the way selected items are being presented - in my case it was given since my tree view items had their own DataTemplate which had indication for its selection.
If this is not your case and you need it, simply extent your tree view item data template to indicate its selection state according to its view model IsSelected property.
Hope this will help someone someday :-)
Have fun!
Gili
This question is still coming up in search engines, so I'll answer it 2 years later.
WPF 4.5 now supports pixel based virtualizing panels.
If you can target 4.5, then just add this to your ListBox:
<Setter Property="VirtualizingPanel.ScrollUnit" Value="Pixel"/>
For what's new in .NET 4.5 (which includes new stuff in WPF 4.5) see http://msdn.microsoft.com/en-us/library/ms171868.aspx
Take a look here (Bea Stollnitz) and here(Dan Crevier); basically you'll need to implement your own container that supports both virtualization and pixel-based scrolling. You can also look at this similar SO question for more details or a possible alternative. Deriving from VirtualizingStackPanel and modifying the scroll behavior seems to be the best bet.
I'm having a heck of a time trying to template bind the StrokeThickness of a rectangle.
My goal is to allow a user of my custom control to set a property called SelectedBorderThickness which will, in fact, set the StrokeThickness of a rectangle.
I thought I understood templating but I guess I really don't.
If I do this:
<Rectangle x:Name="myRect" Height="100" Width="100" Stroke="Black" SelectedBorderThickness="5" />
Can someone please show me how to write the Style elements to get this to work?
You should add more details to the question and people will be able to help you more easily. I think I have figured out what you want though.
You are looking to make a custom templated silverlight control, containing a bunch of elements incluiding a rectangle in its template. You would like a user to be able to set the thickness of that rectangle inside the control with a property on the control itself. From what you put above, I don't know how much you have written in your code -- so I will just post a nearly complete example of what you are after.
First I created a templated custom control in visual studio, and added the new dependancy property we want a user to be able to set:
public class TestControl : Control
{
static public DependencyProperty SBTProperty { get; set; }
static TestControl()
{
SBTProperty = DependencyProperty.Register("SelectedBorderThickness", typeof(double), typeof(TestControl),null);
}
public TestControl()
{
this.DefaultStyleKey = typeof(TestControl);
}
public double SelectedBorderThickness
{
get { return (double)GetValue(SBTProperty); }
set { SetValue(SBTProperty, value); }
}
}
Then I set up the template in Generic.xaml (for my example the only thing I have in my control is the rectangle since I don't know what you want in there):
<Style TargetType="local:TestControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:TestControl">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Rectangle Fill="Bisque" Stroke="Black" StrokeThickness="{TemplateBinding SelectedBorderThickness}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Now I am all set up to use it from xaml in other parts of my application. For my example, I put one right in the center of my MainPage:
<local:TestControl SelectedBorderThickness="75"></local:TestControl>
EDIT:
After reading your code below, I see now what the problem is. You're trying to do a template binding, but the way you have it it's going to try to bind to the current template, which is the template for listboxitem and not your custom listbox. What you really want in this situation is to do a RelativeBinding with FindAncestor to jump up the tree to the template of your custom listbox, but MS hasn't yet implemented that kind of binding in Silverlight (even though it's pretty common in WPF). Luckily in your specific situation we can finagle the right object through the path in a TemplatedParent binding, without having to write a bunch of messy codebehind to emulate an ancestor binding:
StrokeThickness="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Content.Parent.SelectedBorderThickness}"
Dump that into the Rectangle in your template posted above and it should work -- it will access the content of the ListBoxItem (which is whatever you are displaying), and then access that objects Parent (which will be your custom listbox). From there we just hit up the property we set up before.
If you want a cleaner solution, join the chorus of us asking MS to implement ancestor binding in Silverlight.
Here's the problem section, it's when I'm attempting to style the ItemContainerStyle for my custom control which derives from a ListBox:
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Grid Background="{TemplateBinding Background}">
<!-- VSM stuff removed for clarity -->
<ContentPresenter
x:Name="contentPresenter"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
Margin="{TemplateBinding Padding}"/>
<Rectangle x:Name="FocusVisualElement"
Stroke="Goldenrod"
StrokeThickness="{TemplateBinding SelectedBorderThickness}"
Visibility="Collapsed"
RadiusX="1"
RadiusY="1" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
The problems is that when I set StrokeThickness = {TemplateBinding SelectedBorderThickness} on the Rectangle and then try to use the control in a test app, I get a ParserError:
Message: Unknown attribute StrokeThickness on element Rectangle
If I hardcode the StrokeThickness to 3 (or whatever), it parses fine and I can view the test app.
In the end, all I'm really trying to do is create a property that shows up in Intellisense so that my (eventual) end users of my custom control can change the color and border thickness, radius, etc. of the highlight on a hovered and selected ListBoxItem in a dynamically bound custom ListBox. It shouldn't be this dang hard.
The dang comments are too restricted. I'm not trying to answer my own question (I wish I could).
David, your code works fine when you add ListBoxItems statically. When adding them dynamically, the thickness doesn't change. To test this out, I added a new TestControl in MainPage:
<StackPanel>
<local:TestControl SelectedBorderThickness="9" x:Name="h1n1">
<TextBlock Text="Honk1"></TextBlock>
<TextBlock Text="Honk2"/>
</local:TestControl>
<local:TestControl x:Name="SwineFlu" SelectedBorderThickness="20" />
</StackPanel>
In the code-behind I added:
ObservableCollection<string> test = new ObservableCollection<string>();
test.Add("Hi David");
test.Add("Hello World");
SwineFlu.ItemsSource = test;