ArrangeOverride not being called on children? - wpf

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.

Related

ZIndex for one canvas over an adjacent canvas

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"

Adding shapes to Canvas. How to bind to VM

Sorry that the title is a bit vague but I could'n come up with a better one.
For arguments sake let's say I'm developing a simple drawing application, where the user just clicks and drags to draw a line (I'm not really developing that, but just to keep it simple).
I have a custom shape for the line to draw. Now I want to add new lines to the view as needed, but I'd like to use an ObservableCollection property via data binding on the view model to do that. Usaully I would use an ItemsControl. But of course the ItemsControl automatically positions it's items, which is not what I want.
Does anyone have an idea how to do that? Is there a way to disable the layout functions of an ItemsControl?
You can change the ItemsPanelTemplate of an ItemsControl so it uses a Canvas instead of a StackPanel to hold its items, then use the ItemContainerStyle to bind the Canvas.Top and Canvas.Left properties to your data object to position them.
<ItemsControl ItemsSource="{Binding MyCollection}">
<!-- ItemsPanelTemplate -->
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<!-- ItemContainerStyle -->
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Top" Value="{Binding Y}" />
<Setter Property="Canvas.Left" Value="{Binding X}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
I have a blog article about the ItemsControl that explains in more detail how an ItemsControl works if you're interested.

x:Shared MarkupExtension in Silverlight

Is there a workaround for the missing x:Shared MarkupExtension in silverlight?
I have the following Xaml which is creating an ellipse on each target series. I need the ellipses to be unique as they are later added to canvas. By using this Xaml I get the error that the UIElement has already been added to another parent (e.g. single Ellipse instance added to Canvas multiple times).
In WPF I simply use the x:Shared property on this style to get it to work.
<!-- Set the style for the series -->
<Style TargetType="SciChart:FastLineRenderableSeries" >
<Setter Property="SeriesColor" Value="#FF93F2C1"/>
<Setter Property="ResamplingMode" Value="Mid"/>
<Setter Property="RolloverMarker">
<Setter.Value>
<Ellipse Width="9" Height="9" Fill="#7793F2C1" Stroke="#FFA3FFC9"/>
</Setter.Value>
</Setter>
</Style>
A workaround I considered was to create a control called RolloverMarker and set its control template. I'd appreciate any direct or indirect solutions to this problem.
If you are dynamically adding objects to a panel, then a new object needs to be created each time, or you need to define your control in some kind of Template and add a new data object which will use the Template. You cannot add the same item multiple times.
For example,
// Does not work
var templateItem = new FastLineRenderableSeries();
myCanvas.Add(templateItem);
myCanvas.Add(templateItem);
// Works
myCanvas.Add(new FastLineRenderableSeries());
myCanvas.Add(new FastLineRenderableSeries());
Or
<ItemsControl ItemsSource="{Binding SomeCollection}"
ItemTemplate="{StaticResource FastLineRenderableSeriesStyle}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
// Add items. They'll get rendered with defined ItemStyle.
var templateItem = new FastLineRenderableSeries();
SomeCollection.Add(templateItem);
SomeCollection.Add(templateItem);

How to bind to parent position changes in WPF using XAML and no code behind

I have a visio-like interfact but have actual model data behind some of the elements. The elements can be moved by the user.
I use a contentcontrol on a canvas whereby the viewmodels of the elements are places in the content which can then be displayed differently depending on their type but using the same contentcontrol. It is simple to bind the view to the different properties in the viewmodel. However, I have to save the position in the model, and I cannot find a binding solution.
1) The Application.Save Command is handled in the main view model, so I do not have access to the view there. That means I must save the postion data when the elements are moved, or is there a better approach?
2) Assuming that I am right with 1), I am looking to avoid code behind, i.e. I do not want the contentcontrol to deal with the elements that they have in their content. However, so far the code behind version is all I could come up with:
My code behind solution so far:
All model elements implement an interface:
public interface IViewElement
{
String Position { get; set; }
}
And in the contentcontrol:
void ContentControl_LayoutUpdated(object sender, EventArgs e)
{
IViewElement content = this.Content as IViewElement;
content.Position = new Point(Diagram.GetLeft(this), Diagram.GetTop(this)).ToString();
}
The XAML:
<Style TargetType="{x:Type diagram:Item}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type diagram:Item}">
<Grid Canvas.Top="{Binding ElementName=PART_ContentPresenter, Path=Content.Position, Mode=TwoWay}" DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}"
ContextMenu="{x:Null}">
<!-- PART_ContentPresenter -->
<ContentPresenter x:Name="PART_ContentPresenter"
Content="{TemplateBinding Content}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
<DataTemplate DataType="{x:Type local:ViewModel}">
<StackPanel>
...
</StackPanel>
Just encapsulate the codebehind you've used in a Behavior
Why are you using a string to store the position? Use either a Point or two decimal values, and then bind your ContentControl's Canvas.Top and Canvas.Left position to these values using two-way binding.
It will automatically update the model when the Top and Left positions change.
Edit:
Here's an example:
<ContentControl Canvas.Top="{Binding ContentModel.Top, Mode=TwoWay}"
Canvas.Left="{Binding ContentModel.Left, Mode=TwoWay}"
Content="{Binding ContentModel}" />

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