ZIndex for one canvas over an adjacent canvas - wpf

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"

Related

ArrangeOverride not being called on children?

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.

Why does my MenuItem have an Icon when I have overridden the DataTemplate?

I have successfully implement a WPF menu where the top-level items are drawn as large buttons and the lower level items are drawn as standard menu items (see my previous questions here and here).
In my original attempt at this my lower-level item template (SubItemTemplate in the example below) contained an image and a textblock. The result was something that looked like a normal menu item with an empty Icon area and the image next to the text in the text part of the menu item. I was not expecting to see the icon area in the visual display since I thought that the entire visual display would be determined by the contents of my template. The top-level template (TopLevelItemTemplate) does not have any empty icon area visible.
When I removed my image from teh lower-level template and replaced it with a style-setter for the Icon property, I got the display that I wanted.
I do not understand how and why the Icon property exists on my lower-level item DataTemplate.
Here's my code. The property HasParent is used to distinguish menu items that are not top-level (that is, the ones that are drawn with the SubItemTemplate). The section I don't understand is the DataTrigger.
Why is there an Icon property available inside that trigger?
<UserControl.Resources>
<Image x:Key="MenuIconResource16" Height="16" Width="16" Source="{Binding Icon32}" x:Shared="False" />
<HierarchicalDataTemplate x:Key="TopLevelItemTemplate" ItemsSource="{Binding Children}">
<StackPanel VerticalAlignment="Bottom" Orientation="Vertical">
<Image Width="32" Height="32" VerticalAlignment="Center" Source="{Binding Icon32}" ToolTip="{Binding UserHint}" />
<TextBlock Text="{Binding Name}"/>
</StackPanel>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate x:Key="SubItemTemplate" ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Name}"/>
</HierarchicalDataTemplate>
</UserControl.Resources>
<WrapPanel Height="Auto">
<Menu ItemsSource="{Binding DataContext.EventMenu.TopLevel, ElementName=UserControl}" ItemTemplateSelector="{StaticResource MenuItemTemplateSelector}">
<Menu.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Command" Value="{Binding Command}" />
<Setter Property="CommandParameter" Value="{Binding EventType}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding HasParent}" Value="true">
<Setter Property="Icon" Value="{StaticResource MenuIconResource16}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Menu.ItemContainerStyle>
</Menu>
</WrapPanel>
I thought that the entire visual display would be determined by the contents of my template.
#dkozl noted the difference between DataTemplate and Template -- that is the important distinction. A data template is a XAML fragment that the owning control uses as part of the overall control, which may or may not include other (customizable or hard-coded) visual elements, and/or other data templates. The control template is where this visual structure of the control is defined. If you set/override a control template, then your expectation of not seeing any other visual content, will hold true.
The top-level template (TopLevelItemTemplate) does not have any empty icon area visible.
The other thing to note here is that the default style for Menu defines multiple control templates for its MenuItems. These templates are applied depending on the role "TopLevelHeader", "TopLevelItem", "SubmenuHeader", and "SubmenuItem". So you will see different behavior for these different menu items. Take a look at the default styles/templates, which should be illuminating (although they are kind of complex).
Why is there an Icon property available inside that trigger?
A style trigger has the capability of modifying any dependency property of the control it is applied to. Since the style trigger in question is being applied to the MenuItem control, it can modify the Icon dependency property, which is owned by that control.

Scaling images in a WPF ListBox

I've started developing my code using the example from SO WPF: arranging collection items in a grid. Now, to gain cell selection capability, I renamed each ItemsControl to ListBox, because a ListBox is-a ItemsControl (XAMl somewhat simplified):
<ListBox HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" ItemsSource="{Binding YourItems}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<Grid/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemContainerStyle>
<Style>
<Setter Property="Grid.Column" Value="{Binding X}"/>
<Setter Property="Grid.Row" Value="{Binding Y}"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<Image RenderOptions.BitmapScalingMode="LowQuality" Source="{Binding ...ImageSource, Mode=OneWay}">
</Image>
</DataTemplate>
</ListBox.ItemTemplate>
The grid is filled with glyph run test imaged, based on the code here.
Surprisingly it worked - kind of. Selection works. However, in the case of the ItemsControl, there was no scroll bar. Everything scaled nicely. When I made the window smaller, the grid cells shrunk and so did the images. When I made the window larger, everything scaled up.
Now, with the ListBox that's not the case. The images size stays fixed. If the window isn't large enough, there's an horizontal scroll bar and when the window isn't large enough, some of the images are hidden and the user needs to scroll to the right.
So, my question is: If a ListBox is-an ItemControl, why don't my images scale the same? What should I do to correct it?
This is because ListBox and ItemsControl use different styles. You might easily apply the ItemControl's default style to your ListBox:
<ListBox Style="{StaticResource ResourceKey={x:Type ItemsControl}}">

WPF pinning option that does not fill height

I'm looking for a WPF option that shows panels on the sides, and allows you to pin/unpin them. Basically this means a Window, with a main control in the center and multiple different panels on both the left and right sides of this main control. These panels are collapsed by default, with just their headers visible, and if i hover over them they expand out OVER the main control (without displacing the main control), but i also have the option to pin this panel, where it stays permanantly expanded out, this time forcing the main center control to resize.
Now this sounds pretty much like most docking control options, and indeed i've looked at Avalon and MixModes Synergy , but the problem with these options is that their panels fill out the entire height. I want a panel of a specific height to come out when i hover over it, i don't want it to fill out to screen, and i can't really find anything else that does that. Anyone else seen something like this?
Basically my own ideas on how to do this so far involve programmatically moving the panel from one pinned control to another non-pinned control, but this sounds crazy ugly and i'd love alternatives.
Here is a pure XAMl example of how you can achieve Pin/Unpin functionality.
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<DockPanel Grid.Column="1">
<Label Content="Main Content Area" FontSize="22"
VerticalAlignment="Center" HorizontalAlignment="Center"/>
</DockPanel>
<StackPanel HorizontalAlignment="Left">
<StackPanel.Style>
<Style>
<Setter Property="Grid.Column" Value="1"/>
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=LeftPinBtn,Path=IsChecked}" Value="True">
<Setter Property="Grid.Column" Value="0"/>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<ToggleButton Content="Pin/Unpin" x:Name="LeftPinBtn"/>
</StackPanel>
</Grid>
In the above code,
The left panel lies (floats) on the same grid column of the Main content.
When the ToggleButton is checked they jump to their respective
docking grid column.
You may customize this example & include the mouseover events which will show/hide the panels.
Adding a Grid column to the right + HorizontalAlignment="Right"
will give you right panel.
Similarly using Rows (instead of columns) & VerticalAlignment
will add top/bottom pin functionality.

Custom Control TemplateBinding question

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.

Resources