Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF bing maps control polylines/polygons not draw on first add to collection

I'm working on this surface project where we have a bing maps control and where we would like to draw polylines on the map, by using databinding.

The strange behaviour that's occuring is that when I click the Add button, nothing happens on the map. If I move the map little bit, the polyline is drawn on the map. Another scenario that kind of works, is click the add button once, nothing happens, click it again both polylines are drawn. (In my manual collection I have 4 LocationCollections) so the same happens for the 3rd click and the fourth click where again both lines are drawn.

I have totally no idea where to look anymore to fix this. I have tried subscribing to the Layoutupdated events, which occur in both cases. Also added a collectionchanged event to the observablecollection to see if the add is triggered, and yes it is triggered. Another thing I tried is changing the polyline to pushpin and take the first location from the collection of locations in the pipelineviewmodel, than it's working a expected.

I have uploaded a sample project for if you want to see yourself what's happening.

Really hope that someone can point me in the right direction, because i don't have a clue anymore.

Below you find the code that i have written:

I have the following viewmodels:

MainViewModel

public class MainViewModel
{
    private ObservableCollection<PipelineViewModel> _pipelines;

    public ObservableCollection<PipelineViewModel> Pipes
    {
        get { return _pipelines; }
    }

    public MainViewModel()
    {
        _pipelines = new ObservableCollection<PipelineViewModel>();
    }
}

And the PipelineViewModel which has the collection of Locations which implements INotifyPropertyChanged:

PipelineViewModel

public class PipelineViewModel : ViewModelBase
{
    private LocationCollection _locations;

    public string Geometry { get; set; }
    public string Label { get; set; }
    public LocationCollection Locations
    {
        get { return _locations; }
        set
        {
            _locations = value;
            RaisePropertyChanged("Locations");
        }
    }
}

My XAML looks like below:

<s:SurfaceWindow x:Class="SurfaceApplication3.SurfaceWindow1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:s="http://schemas.microsoft.com/surface/2008"
    xmlns:m="clr-namespace:Microsoft.Maps.MapControl.WPF;assembly=Microsoft.Maps.MapControl.WPF" 
    Title="SurfaceApplication3">
    <s:SurfaceWindow.Resources>
        <DataTemplate x:Key="Poly">
            <m:MapPolyline Locations="{Binding Locations}" Stroke="Black" StrokeThickness="5" />
        </DataTemplate>
    </s:SurfaceWindow.Resources>
  <Grid>
        <m:Map ZoomLevel="8" Center="52.332074,5.542302" Name="Map">
            <m:MapItemsControl Name="x" ItemsSource="{Binding Pipes}" ItemTemplate="{StaticResource Poly}" />
        </m:Map>
        <Button Name="add" Width="100" Height="50" Content="Add" Click="add_Click"></Button>
    </Grid>
</s:SurfaceWindow>

And in our codebehind we are setting up the binding and the click event like this:

private int _counter = 0;
private string[] geoLines;

private MainViewModel _mainViewModel = new MainViewModel();

/// <summary>
/// Default constructor.
/// </summary>
public SurfaceWindow1()
{
    InitializeComponent();

    // Add handlers for window availability events
    AddWindowAvailabilityHandlers();

    this.DataContext = _mainViewModel;

    geoLines = new string[4]{ "52.588032,5.979309; 52.491143,6.020508; 52.397391,5.929871; 52.269838,5.957336; 52.224435,5.696411; 52.071065,5.740356",
                                "52.539614,4.902649; 52.429222,4.801025; 52.308479,4.86145; 52.246301,4.669189; 52.217704,4.836731; 52.313516,5.048218",
                                "51.840869,4.394531; 51.8731,4.866943; 51.99841,5.122375; 52.178985,5.438232; 51.8731,5.701904; 52.071065,6.421509",
                                "51.633362,4.111633; 51.923943,6.193542; 52.561325,5.28717; 52.561325,6.25946; 51.524125,5.427246; 51.937492,5.28717" };
}

private void add_Click(object sender, RoutedEventArgs e)
{
    PipelineViewModel plv = new PipelineViewModel();
    plv.Locations = AddLinestring(geoLines[_counter]);
    plv.Geometry = geoLines[_counter];

    _mainViewModel.Pipes.Add(plv);

    _counter++;
}

private LocationCollection AddLinestring(string shapegeo)
{
    LocationCollection shapeCollection = new LocationCollection();

    string[] lines = Regex.Split(shapegeo, ";");
    foreach (string line in lines)
    {
        string[] pts = Regex.Split(line, ",");

        double lon = double.Parse(pts[1], new CultureInfo("en-GB"));
        double lat = double.Parse(pts[0], new CultureInfo("en-GB"));
        shapeCollection.Add(new Location(lat, lon));
    }

    return shapeCollection;
}
like image 429
ChristiaanV Avatar asked Jun 08 '12 14:06

ChristiaanV


1 Answers

I did some digging on this problem and found that there is a bug in the Map implementation. I also made a workaround for it which can be used like this

<m:Map ...>
    <m:MapItemsControl Name="x"
                       behaviors:MapFixBehavior.FixUpdate="True"/>
</m:Map>

I included this fix in your sample application and uploaded it here: SurfaceApplication3.zip


The visual tree for each ContentPresenter looks like this

enter image description here

When you add a new item to the collection the Polygon gets the wrong Points initially. Instead of values like 59, 29 it gets something like 0.0009, 0.00044.

The points are calculated in MeasureOverride in MapShapeBase and the part that does the calculation looks like this

MapMath.TryLocationToViewportPoint(ref this._NormalizedMercatorToViewport, location, out point2);

Initially, _NormalizedMercatorToViewport will have its default values (everything is set to 0) so the calculations goes all wrong. _NormalizedMercatorToViewport gets set in the method SetView which is called from MeasureOverride in MapLayer.

MeasureOverride in MapLayer has the following two if statements.

if ((element is ContentPresenter) && (VisualTreeHelper.GetChildrenCount(element) > 0))
{
    child.SetView(...)
}

This comes out as false because the ContentPresenter hasn't got a visual child yet, it is still being generated. This is the problem.

The second one looks like this

IProjectable projectable2 = element as IProjectable;
if (projectable2 != null)
{
    projectable2.SetView(...);
}

This comes out as false as well because the element, which is a ContentPresenter, doesn't implement IProjectable. This is implemented by the child MapShapeBase and once again, this child hasn't been generated yet.

So, SetView never gets called and _NormalizedMercatorToViewport in MapShapeBase will have its default values and the calculations goes wrong the first time when you add a new item.


Workaround

To workaround this problem we need to force a re-measure of the MapLayer. This has to be done when a new ContentPresenter is added to the MapItemsControl but after the ContentPresenter has a visual child.

One way to force an update is to create an attached property which has the metadata-flags AffectsRender, AffectsArrange and AffectsMeasure set to true. Then we just change the value of this property everytime we want to do the update.

Here is an attached behavior which does this. Use it like this

<m:Map ...>
    <m:MapItemsControl Name="x"
                       behaviors:MapFixBehavior.FixUpdate="True"/>
</m:Map>

MapFixBehavior

public class MapFixBehavior
{
    public static DependencyProperty FixUpdateProperty =
        DependencyProperty.RegisterAttached("FixUpdate",
                                            typeof(bool),
                                            typeof(MapFixBehavior),
                                            new FrameworkPropertyMetadata(false,
                                                                          OnFixUpdateChanged));

    public static bool GetFixUpdate(DependencyObject mapItemsControl)
    {
        return (bool)mapItemsControl.GetValue(FixUpdateProperty);
    }
    public static void SetFixUpdate(DependencyObject mapItemsControl, bool value)
    {
        mapItemsControl.SetValue(FixUpdateProperty, value);
    }

    private static void OnFixUpdateChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
    {
        MapItemsControl mapItemsControl = target as MapItemsControl;
        ItemsChangedEventHandler itemsChangedEventHandler = null;
        itemsChangedEventHandler = (object sender, ItemsChangedEventArgs ea) =>
        {
            if (ea.Action == NotifyCollectionChangedAction.Add)
            {
                EventHandler statusChanged = null;
                statusChanged = new EventHandler(delegate
                {
                    if (mapItemsControl.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
                    {
                        mapItemsControl.ItemContainerGenerator.StatusChanged -= statusChanged;
                        int index = ea.Position.Index + ea.Position.Offset;
                        ContentPresenter contentPresenter =
                            mapItemsControl.ItemContainerGenerator.ContainerFromIndex(index) as ContentPresenter;
                        if (VisualTreeHelper.GetChildrenCount(contentPresenter) == 1)
                        {
                            MapLayer mapLayer = GetVisualParent<MapLayer>(mapItemsControl);
                            mapLayer.ForceMeasure();
                        }
                        else
                        {
                            EventHandler layoutUpdated = null;
                            layoutUpdated = new EventHandler(delegate
                            {
                                if (VisualTreeHelper.GetChildrenCount(contentPresenter) == 1)
                                {
                                    contentPresenter.LayoutUpdated -= layoutUpdated;
                                    MapLayer mapLayer = GetVisualParent<MapLayer>(mapItemsControl);
                                    mapLayer.ForceMeasure();
                                }
                            });
                            contentPresenter.LayoutUpdated += layoutUpdated;
                        }
                    }
                });
                mapItemsControl.ItemContainerGenerator.StatusChanged += statusChanged;
            }
        };
        mapItemsControl.ItemContainerGenerator.ItemsChanged += itemsChangedEventHandler;
    }

    private static T GetVisualParent<T>(object childObject) where T : Visual
    {
        DependencyObject child = childObject as DependencyObject;
        while ((child != null) && !(child is T))
        {
            child = VisualTreeHelper.GetParent(child);
        }
        return child as T;
    }
}

MapLayerExtensions

public static class MapLayerExtensions
{
    private static DependencyProperty ForceMeasureProperty =
        DependencyProperty.RegisterAttached("ForceMeasure",
                                            typeof(int),
                                            typeof(MapLayerExtensions),
                                            new FrameworkPropertyMetadata(0,
                                                FrameworkPropertyMetadataOptions.AffectsRender |
                                                FrameworkPropertyMetadataOptions.AffectsArrange |
                                                FrameworkPropertyMetadataOptions.AffectsMeasure));

    private static int GetForceMeasure(DependencyObject mapLayer)
    {
        return (int)mapLayer.GetValue(ForceMeasureProperty);
    }
    private static void SetForceMeasure(DependencyObject mapLayer, int value)
    {
        mapLayer.SetValue(ForceMeasureProperty, value);
    }

    public static void ForceMeasure(this MapLayer mapLayer)
    {
        SetForceMeasure(mapLayer, GetForceMeasure(mapLayer) + 1);
    }
}
like image 109
Fredrik Hedblad Avatar answered Sep 22 '22 18:09

Fredrik Hedblad