none
WPF Bing Maps Control MapLayer.Position not updated

    Întrebare

  • I'm using the Bing Maps WPF control in a MVVM application. The Map control's DataContext is bound to a MapViewModel class which has a Symbols property of type ObservableCollection<MapSymbolViewModel>.

    A MapItemsControl is used to generate a collection of symbol views (=visual representations of type MapView, derived from UserControl) on a map from the view model layer. The MapSymbolViewModel class has a Position property of type Microsoft.Maps.MapControl.WPF.Location. The symbols are positioned correctly on the map when the map initially shows up, and they are also correctly added and removed as I add MapSymbolViewModel instances to the ObservableCollection.

    But the symbol positions are not updated immediately when the MapSymbolViewModel.Position property changes. The MapSymbolViewModel class implements the INotifyPropertyChanged interface and properly raises a PropertyChangedEvent on the UI thread with PropertyName="Position" when the position is modified in the view model layer. But this change is not reflected immediately by the Map control in the view layer. The position is only updated when a Map control "redraw" is triggered by some other updates (I also have databound MapPolygons on the map which trigger an update).

    To me this looks like a bug in the binding logic of the attached MapLayer.Position property.

    Here's my XAML:

    <m:Map x:Name="Map" 
           AnimationLevel="Full"
           Mode="Aerial"
           UseInertia="true">
    
      <!-- Symbols Layer -->
      <m:MapItemsControl ItemsSource="{Binding Symbols}">
        <m:MapItemsControl.ItemTemplate>
          <DataTemplate>
            <Views:MapSymbolView m:MapLayer.Position="{Binding Position}" />
          </DataTemplate>
        </m:MapItemsControl.ItemTemplate>
      </m:MapItemsControl>
    </m:Map>

    Edit: I didn't receive an answer yet, maybe because I didn't ask a question;-)

    My questions are: Can anybody from the dev team confirm that there is a bug? Or am I doing somthing wrong? What is the recommended workaround? (Currently I add and immediately remove a dummy element to the bound "Symbols" collection to trigger an update). What is the roadmap for the WPF Bing Maps control? When can we expect a new version with bugfixes?

    13 martie 2012 07:00

Răspunsuri

  • Hi Duncan,

    thanks a lot for your suggestion. I tried it, but unfortunately it didn't help. Explicitly calling d.SetValue(MapLayer.PositionProperty, e.NewValue) didn't change a thing because the cause of the problem is lying somewhere else (see below).

    But your idea brought some new insights so that I was finally able to provide a viable workaround for my problem. I solved it by deriving a MapLayerV2 class from the Bing Maps MapLayer class and providing a bugfixed implementation of the PositionProperty there.

    For those interested in the bugfix only, here's the code:

      /// <summary>
      /// Enhanced version of the Bing Maps <see cref="MapLayer"/> class with
      /// a bugfix for the missing update when the <see cref="MapLayer.PositionProperty"/>
      /// attached property changed.
      /// </summary>
      public class MapLayerV2 : MapLayer
      {
        #region Position attached property
    
        public new static Location GetPosition(DependencyObject obj)
        {
          return (Location) obj.GetValue(PositionProperty);
        }
    
        public new static void SetPosition(DependencyObject obj, Location value)
        {
          obj.SetValue(PositionProperty, value);
        }
    
        // Using a DependencyProperty as the backing store for Position2.  This enables animation, styling, binding, etc...
        public new static readonly DependencyProperty PositionProperty =
          DependencyProperty.RegisterAttached("Position", typeof (Location), typeof (MapLayerV2),
                                              new PropertyMetadata(OnPositionChanged));
    
        private new static void OnPositionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
          // Delegate to base class property
          d.SetValue(MapLayer.PositionProperty, e.NewValue);
    
          // Call alternate implementation of the private InvalidateParentLayout function
          InvalidateParentLayout(d);
        }
    
        #endregion
    
        /// <summary>
        /// Provides an alternate implementation of the private <c>MapLayer.InvalidateParentLayer</c>
        /// method. 
        /// </summary>
        /// <param name="dependencyObject"></param>
        /// <remarks>
        /// The base class implementation has a bug which prevents it to function correctly
        /// on elements generated through a <see cref="DataTemplate"/> (like items contained within
        /// a <see cref="MapItemsControl"/>). For these elements the <see cref="FrameworkElement.Parent"/>
        /// is <c>null</c>. The base class implementation uses this property to navigate up the <c>logical</c>
        /// tree to find the parent <see cref="MapLayer"/> element. This version uses the <c>visual</c>
        /// tree instead to find the ancestor <see cref="MapLayer"/> element.
        /// </remarks>
        private static void InvalidateParentLayout(DependencyObject dependencyObject)
        {
          var element = dependencyObject as FrameworkElement;
          var mapLayer = element.FindVisualParent<MapLayer>();
          if (mapLayer != null)
          {
            mapLayer.InvalidateMeasure();
          }
        }
      }
    

    The code makes use of the FindVisualParent helper method which I implemented as an extension class:

      public static class DependencyObjectExtensions
      {
        public static TParentItem FindVisualParent<TParentItem>(this DependencyObject child)
          where TParentItem : Visual
        {
          if (child == null)
            return null;
    
          child = VisualTreeHelper.GetParent(child);
          if (child == null || child is TParentItem)
            return (TParentItem)child;
    
          return FindVisualParent<TParentItem>(child);
        }
      }
    

    And here's how to use it:

    <m:MapItemsControl ItemsSource="{Binding Symbols}">
      <m:MapItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
          <Views:MapLayerV2 />
        </ItemsPanelTemplate>
      </m:MapItemsControl.ItemsPanel>
      <m:MapItemsControl.ItemTemplate>
        <DataTemplate>
          <Views:MapSymbolView Views:MapLayerV2.Position="{Binding Position}"
                               m:MapLayer.PositionOrigin="{Binding PositionOrigin}" />
        </DataTemplate>
      </m:MapItemsControl.ItemTemplate>
    </m:MapItemsControl>
    

    The key point and root cause of the problem is, that the PropertyChangedCallback implementation of the original MapLayer class is insufficient. The original code uses a somewhat naive approach to find the parent MapLayer instance which contains the element on which the MapLayer.Position attached property is set. It thereby relys on the value of the FrameworkElement.Parent property to navigate up the logical tree in an attempt to find the parent MapLayer element.

    But: The Parent property is null for elements generated through a DataTemplate which is usually the case for elements generated by an ItemsControl! (Side note: For these elements, only the TemplatedParent property is set.)

    This means that the bug only affects elements generated by a MapItemsControl. Elements which are immediate children of a MapLayer are not affected.

    @Microsoft: Would be of great help if you could fix. Please!

    There's one minor issue left: The only thing my bugfix needs is an improved PropertyChangedCallback for the MapLayer.PositionProperty attached property. I tried to "install" such a callback in my MapLayerV2 class through OverrideMetadata like this:

    static MapLayerV2()
    {
      PositionProperty.OverrideMetadata(typeof(MapLayerV2), new PropertyMetadata(OnPositionChanged));
    }

    But this didn't work. The OnPositionChanged handler doesn't get called. Don't know why. That's why I had to do the ugly "new" PositionProperty implementation. Maybe someone with more experience on the WPF dependency property system can help me out.

    Thanks again to Duncan for putting me on the right track.

    • Marcat ca răspuns de candritzky 21 martie 2012 12:39
    21 martie 2012 12:38

Toate mesajele

  • Good Morning

    There are some dependency properties that do not handle binding in quite the way you expect. i.e. they do not refresh when the bound value changes.

    In cases where I have struck this I have built my own Attached dependency properties to ensure the updates occur.

    Here is a snippet of an un-tested possible implementation of an Attached Dependency Property that may help to push property changes through to the position that the symbol will use

    Public Shared ReadOnly MapLayerPositionProperty As DependencyProperty =
                  DependencyProperty.RegisterAttached("MapLayerPosition",
                                                     GetType(Location),
                                                     GetType(WpfAttachedPropertiesLib),
                                                     New PropertyMetadata(Nothing, New PropertyChangedCallback(AddressOf OnMapLayerPositionChanged)))
    
        Public Shared Function GetMapLayerPosition(d As DependencyObject) As Location
            Return d.GetValue(MapLayerPositionProperty)
        End Function
    
        Public Shared Sub SetMapLayerPosition(d As DependencyObject, value As Location)
            d.SetValue(MapLayerPositionProperty, value)
        End Sub
    
        Private Shared Sub OnMapLayerPositionChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
            d.SetValue(MapLayer.PostionProperty, e.NewValue)
        End Sub

    You would then have to reference the xmlns for the namspace the implementation is in then you can use the attached property in the same way as you are using the MapLayer.PositionProperty attached property.

    <Views:MapSymbolView local:MapAttachedPropertiesLib.MapLayerPosition="{Binding Position}" />
    

    I hope this helps.

    Please let me know if you have any issues or if this is helpful.

    Regards,

    Duncan McGregor

    http://DRMcG.wordpress.com

    20 martie 2012 22:56
  • Hi Duncan,

    thanks a lot for your suggestion. I tried it, but unfortunately it didn't help. Explicitly calling d.SetValue(MapLayer.PositionProperty, e.NewValue) didn't change a thing because the cause of the problem is lying somewhere else (see below).

    But your idea brought some new insights so that I was finally able to provide a viable workaround for my problem. I solved it by deriving a MapLayerV2 class from the Bing Maps MapLayer class and providing a bugfixed implementation of the PositionProperty there.

    For those interested in the bugfix only, here's the code:

      /// <summary>
      /// Enhanced version of the Bing Maps <see cref="MapLayer"/> class with
      /// a bugfix for the missing update when the <see cref="MapLayer.PositionProperty"/>
      /// attached property changed.
      /// </summary>
      public class MapLayerV2 : MapLayer
      {
        #region Position attached property
    
        public new static Location GetPosition(DependencyObject obj)
        {
          return (Location) obj.GetValue(PositionProperty);
        }
    
        public new static void SetPosition(DependencyObject obj, Location value)
        {
          obj.SetValue(PositionProperty, value);
        }
    
        // Using a DependencyProperty as the backing store for Position2.  This enables animation, styling, binding, etc...
        public new static readonly DependencyProperty PositionProperty =
          DependencyProperty.RegisterAttached("Position", typeof (Location), typeof (MapLayerV2),
                                              new PropertyMetadata(OnPositionChanged));
    
        private new static void OnPositionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
          // Delegate to base class property
          d.SetValue(MapLayer.PositionProperty, e.NewValue);
    
          // Call alternate implementation of the private InvalidateParentLayout function
          InvalidateParentLayout(d);
        }
    
        #endregion
    
        /// <summary>
        /// Provides an alternate implementation of the private <c>MapLayer.InvalidateParentLayer</c>
        /// method. 
        /// </summary>
        /// <param name="dependencyObject"></param>
        /// <remarks>
        /// The base class implementation has a bug which prevents it to function correctly
        /// on elements generated through a <see cref="DataTemplate"/> (like items contained within
        /// a <see cref="MapItemsControl"/>). For these elements the <see cref="FrameworkElement.Parent"/>
        /// is <c>null</c>. The base class implementation uses this property to navigate up the <c>logical</c>
        /// tree to find the parent <see cref="MapLayer"/> element. This version uses the <c>visual</c>
        /// tree instead to find the ancestor <see cref="MapLayer"/> element.
        /// </remarks>
        private static void InvalidateParentLayout(DependencyObject dependencyObject)
        {
          var element = dependencyObject as FrameworkElement;
          var mapLayer = element.FindVisualParent<MapLayer>();
          if (mapLayer != null)
          {
            mapLayer.InvalidateMeasure();
          }
        }
      }
    

    The code makes use of the FindVisualParent helper method which I implemented as an extension class:

      public static class DependencyObjectExtensions
      {
        public static TParentItem FindVisualParent<TParentItem>(this DependencyObject child)
          where TParentItem : Visual
        {
          if (child == null)
            return null;
    
          child = VisualTreeHelper.GetParent(child);
          if (child == null || child is TParentItem)
            return (TParentItem)child;
    
          return FindVisualParent<TParentItem>(child);
        }
      }
    

    And here's how to use it:

    <m:MapItemsControl ItemsSource="{Binding Symbols}">
      <m:MapItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
          <Views:MapLayerV2 />
        </ItemsPanelTemplate>
      </m:MapItemsControl.ItemsPanel>
      <m:MapItemsControl.ItemTemplate>
        <DataTemplate>
          <Views:MapSymbolView Views:MapLayerV2.Position="{Binding Position}"
                               m:MapLayer.PositionOrigin="{Binding PositionOrigin}" />
        </DataTemplate>
      </m:MapItemsControl.ItemTemplate>
    </m:MapItemsControl>
    

    The key point and root cause of the problem is, that the PropertyChangedCallback implementation of the original MapLayer class is insufficient. The original code uses a somewhat naive approach to find the parent MapLayer instance which contains the element on which the MapLayer.Position attached property is set. It thereby relys on the value of the FrameworkElement.Parent property to navigate up the logical tree in an attempt to find the parent MapLayer element.

    But: The Parent property is null for elements generated through a DataTemplate which is usually the case for elements generated by an ItemsControl! (Side note: For these elements, only the TemplatedParent property is set.)

    This means that the bug only affects elements generated by a MapItemsControl. Elements which are immediate children of a MapLayer are not affected.

    @Microsoft: Would be of great help if you could fix. Please!

    There's one minor issue left: The only thing my bugfix needs is an improved PropertyChangedCallback for the MapLayer.PositionProperty attached property. I tried to "install" such a callback in my MapLayerV2 class through OverrideMetadata like this:

    static MapLayerV2()
    {
      PositionProperty.OverrideMetadata(typeof(MapLayerV2), new PropertyMetadata(OnPositionChanged));
    }

    But this didn't work. The OnPositionChanged handler doesn't get called. Don't know why. That's why I had to do the ugly "new" PositionProperty implementation. Maybe someone with more experience on the WPF dependency property system can help me out.

    Thanks again to Duncan for putting me on the right track.

    • Marcat ca răspuns de candritzky 21 martie 2012 12:39
    21 martie 2012 12:38
  • @candritzky,

    I can also confirm this bug.  I was going nuts trying to figure out why the Location of my Pushpins weren't being updated.  I followed your approach and can confirm that manually calling the MapLayer's InvalidateMeasure method addresses the situation.  In my case I called it in my custom Pushpin class.  Is there a place we are supposed to report these kinds of bugs, or is MS looking through these forums?


    -Jonathan

    2 aprilie 2012 17:56
  • There are a number of Bing Maps people from Microsoft who frequent the forums to answer questions and get feedback about issues, bugs and feature requests. In fact the developers of the WPF control often review this forum as well.

    http://rbrundritt.wordpress.com

    2 aprilie 2012 18:40