14

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;
}
Fredrik Hedblad
  • 79,694
  • 21
  • 251
  • 262
ChristiaanV
  • 5,181
  • 1
  • 29
  • 42
  • I can't help you on this but have tested your sample project; made some trial and error stuff (Invalidate, force moving the map) but also don't have a clue why it isn't working. Everything you've implemented looks fine. But here are some findings: Adding the MapPolyline from CodeBehind works fine. If you use another element like Pushpin it also works fine. So the issue has to do with all stuff that inherits from MapShapeBase. And that are MapPolyline and MapPolygon. I had a look on it via Reflector and tried to compare the Pushpin implementation with the MapPolyline implementation. – SvenG Jun 11 '12 at 16:43
  • I can't invest more time, but if I could I'd debug the reflectored code and have a look why a Pushpin is refreshed correctly but a MapPolyLine/MapPolygon isn't. – SvenG Jun 11 '12 at 16:45
  • Hi SvenG, Thanks for your time looking at it. Yes, I have seen that the pushpins just work fine. I also have called the method UpdateLayout() on the MapItemsControl and than add an empty UIElement to the layer and it will show the polyline. Still hav eno clue why it's not working :( – ChristiaanV Jun 12 '12 at 07:21
  • @Clemens Sorry, I have to use the Bing maps control. – ChristiaanV Jun 14 '12 at 14:23

1 Answers1

17

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);
    }
}
Fredrik Hedblad
  • 79,694
  • 21
  • 251
  • 262
  • @ChristiaanV: Any luck with the workaround I suggested? If you have any questions regarding the "bug" I'm happy to try and answer. The way I see it, the problem isn't with your implementation but with the implementation of the `Map` from Microsoft. I think that this is a problem that needs to be fixed on their end and the only way to get it to work until they do is to use some sort of a workaround. – Fredrik Hedblad Jun 17 '12 at 18:33
  • Wow, that's an great answer! Really happy with that! – ChristiaanV Jun 18 '12 at 05:48
  • 1
    I have been looking for a fix to this problem for some time and whilst there have been various suggestions this is the only one that (so far) has worked reliably. One thing though; perhaps I am structuring my XAML incorrectly but if you place the MapItemsControl inside a MapLayer then the fix does not work. Thankfully I do not really need the multiple layers for what I am doing. – Martin Robins Dec 05 '13 at 12:53