Fellow WPF users,
I'm currently trying to wrap my head around a problem related to WPF Blend behaviors. I've found a behavior I'd like to use in a WPF application, but the code was originally written for Silverlight some time ago (http://www.sharpgis.net/post/2009/08/11/Silverlight-Behaviors-Triggers-and-Actions.aspx).
The problem I have is that when the animation starts and the dependency property OffsetMediatorProperty changes, and I get into the method OnOffsetMediatorPropertyChanged, the AssociatedObject is null. In addition, it also looks like all the fields are null.
The behavior I'm using:
public class MouseScrollViewer : Behavior<ScrollViewer>
{
double target = 0;
int direction = 0;
private Storyboard storyboard;
private DoubleAnimation animation;
protected override void OnAttached()
{
base.OnAttached();
CreateStoryBoard();
AssociatedObject.PreviewMouseWheel += AssociatedObject_MouseWheel;
}
protected override void OnDetaching()
{
AssociatedObject.PreviewMouseWheel -= AssociatedObject_MouseWheel;
storyboard = null;
base.OnDetaching();
}
private void AssociatedObject_MouseWheel(object sender, MouseWheelEventArgs e)
{
if (Animate(-Math.Sign(e.Delta) * ScrollAmount))
{
e.Handled = true;
}
}
private bool Animate(double offset)
{
storyboard.Pause();
if (Math.Sign(offset) != direction)
{
target = AssociatedObject.VerticalOffset;
direction = Math.Sign(offset);
}
target += offset;
target = Math.Max(Math.Min(target, AssociatedObject.ScrollableHeight), 0);
animation.To = target;
animation.From = AssociatedObject.VerticalOffset;
if (animation.From != animation.To)
{
storyboard.Begin();
return true;
}
return false;
}
private void CreateStoryBoard()
{
storyboard = new Storyboard();
animation = new DoubleAnimation
{
Duration = TimeSpan.FromSeconds(.5),
EasingFunction = new ExponentialEase { EasingMode = EasingMode.EaseOut }
};
Storyboard.SetTarget(animation, this);
Storyboard.SetTargetProperty(animation, new PropertyPath(OffsetMediatorProperty));
storyboard.Children.Add(animation);
storyboard.Completed += (s, e) => { direction = 0; };
}
internal double OffsetMediator
{
get { return (double)GetValue(OffsetMediatorProperty); }
set { SetValue(OffsetMediatorProperty, value); }
}
internal static readonly DependencyProperty OffsetMediatorProperty =
DependencyProperty.Register("OffsetMediator", typeof(double), typeof(MouseScrollViewer), new PropertyMetadata(0.0, OnOffsetMediatorPropertyChanged));
private static void OnOffsetMediatorPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MouseScrollViewer msv = d as MouseScrollViewer;
if (msv != null && msv.AssociatedObject != null)
{
msv.AssociatedObject.ScrollToVerticalOffset((double)e.NewValue);
}
}
public double ScrollAmount
{
get { return (double)GetValue(ScrollAmountProperty); }
set { SetValue(ScrollAmountProperty, value); }
}
public static readonly DependencyProperty ScrollAmountProperty =
DependencyProperty.Register("ScrollAmount", typeof(double), typeof(MouseScrollViewer), new PropertyMetadata(50.0));
}
Usage:
<ScrollViewer>
<i:Interaction.Behaviors>
<local:MouseScrollViewer x:Name="ScrollViewer" />
</i:Interaction.Behaviors>
<ItemsControl ItemsSource="{Binding}"
FontSize="32">
</ItemsControl>
</ScrollViewer>
It looks like the behavior works in the Silverlight demo, but not in a WPF application. I really hope some of you are able to explain me why this happens, and hopefully help me sort out this issue.
When storyboard is run using Begin(), it internally tries to access freezable animation object which in your case is instance object of class MouseScrollViewer and hence it ends up cloning an instance of animation and eventually your MosueScrollViewer.
So, actual issue is AssociatedObject is null because animation is done on another instance of MouseScrollViewer and not on your actual instance.
There are two workarounds for this:
First call animation.Freeze() on animation object so that new instance is not created but issue with this approach is it will work first time only and second time it will fail with that freeze objects properties can't be modified.
Second workaround would be to, you don't need storyboard at all when you need animation from code. You can directly do animation on your current instance. Look below for the changes you need to make to do that:
First of all remove the storyboard completely from your code.
Second modify CreateStoryBoard() to CreateAnimation():
private void CreateAnimation()
{
animation = new DoubleAnimation
{
Duration = TimeSpan.FromSeconds(.5),
EasingFunction = new ExponentialEase { EasingMode = EasingMode.EaseOut }
};
Storyboard.SetTarget(animation, this);
Storyboard.SetTargetProperty(animation, new
PropertyPath(OffsetMediatorProperty));
animation.Completed += (s, e) => { direction = 0; };
}
and Animate() method should go like this:
private bool Animate(double offset)
{
if (Math.Sign(offset) != direction)
{
target = AssociatedObject.VerticalOffset;
direction = Math.Sign(offset);
}
target += offset;
target = Math.Max(Math.Min(target, AssociatedObject.ScrollableHeight), 0);
animation.To = target;
animation.From = AssociatedObject.VerticalOffset;
if (animation.From != animation.To)
{
this.BeginAnimation(OffsetMediatorProperty, animation); <-- HERE
return true;
}
return false;
}
Related
I have make a ContentControl and it has some custom Propertities. The control itself works fine but I like to update its interface during design time in XAML editor. The problem is next: The control's UI update if I change its Size (SizeChanged event will do that) but I cannot find any way to do this if CustomProperty like OffsetX changes during design time.
So, how to change the following code to make this happen? It isn't too convenient to update Control UI changing its size every time.
public sealed class MyControlElement: ContentControl
{
//
//SOME INITIALIZE CODE IS HERE
//
public MyControlElement() => DefaultStyleKey = typeof(MyControlElement);
protected override void OnApplyTemplate()
{
//
//SOME INITIALIZE CODE IS HERE
//
base.OnApplyTemplate();
}
//OFFSET X DESCRIPTION
[Description("OffsetX"), Category("MyControlElementParameters"), Browsable(true)]
//OFFSET X
public int OffsetX
{
get
{
return (int)GetValue(OffsetXProperty);
}
set
{
if (OffsetX != value)
{
SetValue(OffsetXProperty, value);
OnOffsetXChanged(this, new EventArgs());
}
}
}
public static readonly DependencyProperty OffsetXProperty = DependencyProperty.Register("OffsetX", typeof(int), typeof(MyControlElement), PropertyMetadata.Create(0));
public event EventHandler OffsetXChanged;
private void OnOffsetXChanged(object sender, EventArgs e)
{
UpdateControlUI();
this.OffsetXChanged?.Invoke(this, e);
}
}
I found some kind of "Hack". Still hoping to find better solution. The next trick works and it is possible to update Control interface during design time.
First need to add handler for Loaded.
public MyControlElement()
{
this.DefaultStyleKey = typeof(MyControlElement);
this.Loaded += MyControlElement_Loaded;
}
private void MyControlElement_Loaded(object sender, RoutedEventArgs e)
{
//
//SOME INITIALIZE CODE HERE IF NEEDED
//
//RUN CONTROL VISUAL UPDATER ONLY IF IN DESIGN MODE
if (DesignMode.DesignModeEnabled) ControlDesignTimeUIUpdater();
//FLAG - CONTROL HAS BEEN INITIALIZED
IsControlInitialized = true;
}
And lets add ControlDesignTimeUIUpdater void for UI update. This void has a loop to keep UI updated during design time.
private async void ControlDesignTimeUIUpdater()
{
double OldImageWidth = ImageWidth;
double OldImageHeight = ImageHeight;
CornerRadius OldImageCornerRadius = ImageCornerRadius;
double OldBorderThickness = BorderThickness;
ImageSource OldMyImageSource = MyImageSource;
while (this.IsLoaded)
{
//CHECK CHANGES DELAY 100ms
await Task.Delay(100);
//MAKE SURE CONTROL IS INITIALIZED BEFORE ANY UI UPDATES
if (IsControlInitialized)
{
if (OldImageWidth != ImageWidth)
{
OldImageWidth = ImageWidth;
SetImageWidth();
}
if (OldImageHeight != ImageHeight)
{
OldImageHeight = ImageHeight;
SetImageHeight();
}
if (OldImageCornerRadius != ImageCornerRadius)
{
OldImageCornerRadius = ImageCornerRadius;
SetImageCornerRadius();
}
if (OldBorderThickness != BorderThickness)
{
OldBorderThickness = BorderThickness;
SetBorderThickness();
}
if (OldMyImageSource != MyImageSource)
{
OldMyImageSource = MyImageSource;
SetMyImageSource();
}
//
// ETC.
//
}
}
}
By this Hack it is possible update control in "real-time" during design. It's even possible add animations, size changes etc.
I am creating a button with a gradient in Xamarin forms. I am successfully making it but when i later on in the code try to update its color, nothing happens in the UI.
This is how the project is setup:
XAML:
<controls:FullyColoredGradient x:Name = "SelectedBackground" StartColor = "Purple" EndColor="Yellow" />
If i then later on the code do a void and try to update these colors like this:
SelectedBackground.EndColor = Color.Red;
SelectedBackground.StartColor = Color.Blue;
Then nothing happens. They do not recolor.
This is how my shared code looks:
public class FullyColoredGradient : Button
{
public static readonly BindableProperty StartColorProperty =
BindableProperty.Create(nameof(StartColor),
typeof(Color), typeof(FullyColoredGradient),
Color.Default);
public Color StartColor
{
get { return (Color)GetValue(StartColorProperty); }
set { SetValue(StartColorProperty, value); }
}
public static readonly BindableProperty EndColorProperty =
BindableProperty.Create(nameof(EndColor),
typeof(Color), typeof(FullyColoredGradient),
Color.Default);
public Color EndColor
{
get { return (Color)GetValue(EndColorProperty); }
set { SetValue(EndColorProperty, value); }
}
}
And this is my iOS renderer:
public class TransparentGradientColor_iOS : ButtonRenderer
{
CGRect rect;
CAGradientLayer gradientLayer;
public TransparentGradientColor_iOS() { }
public override void Draw(CGRect rect)
{
base.Draw(rect);
this.rect = rect;
FullyColoredGradient rcv = (FullyColoredGradient)Element;
if (rcv == null)
return;
this.ClipsToBounds = true;
this.Layer.MasksToBounds = true;
FullyColoredGradient stack = (FullyColoredGradient)this.Element;
CGColor startColor = stack.StartColor.ToCGColor();
CGColor endColor = stack.EndColor.ToCGColor();
#region for Vertical Gradient
this.gradientLayer = new CAGradientLayer()
{
StartPoint = new CGPoint(0, 0.5),
EndPoint = new CGPoint(1, 0.5)
};
#endregion
gradientLayer.Frame = rect;
gradientLayer.Colors = new CGColor[] { startColor, endColor };
NativeView.Layer.InsertSublayer(gradientLayer, 0);
}
protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
{
base.OnElementChanged(e);
if (e.OldElement == null)
{
UpdateColor();
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
UpdateColor();
}
void UpdateColor()
{
if (gradientLayer != null)
{
FullyColoredGradient stack = (FullyColoredGradient)this.Element;
CGColor startColor = Xamarin.Forms.Color.White.ToCGColor();
CGColor endColor = Xamarin.Forms.Color.White.ToCGColor();
gradientLayer.Colors = new CGColor[] { startColor, endColor };
}
}
}
I have used this custom renderer, this will apply gradient to all buttons(you can create custom button if you want), you can try this out:
[assembly: ExportRenderer(typeof(Button), typeof(CustomButtonRendereriOS))]
namespace XYZ.iOS.Renderer
{
public class CustomButtonRendereriOS : ButtonRenderer
{
//To apply gradient background to button
public override CGRect Frame
{
get
{
return base.Frame;
}
set
{
if (value.Width > 0 && value.Height > 0)
{
foreach (var layer in Control?.Layer.Sublayers.Where(layer => layer is CAGradientLayer))
layer.Frame = new CGRect(0, 0, value.Width, value.Height);
}
base.Frame = value;
}
}
protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
{
base.OnElementChanged(e);
if (e.OldElement == null)
{
try
{
var gradient = new CAGradientLayer();
gradient.CornerRadius = Control.Layer.CornerRadius = 5;
gradient.Colors = new CGColor[]
{
UIColor.FromRGB(243, 112, 33).CGColor,
UIColor.FromRGB(226, 64, 64).CGColor
};
var layer = Control?.Layer.Sublayers.LastOrDefault();
Control?.Layer.InsertSublayerBelow(gradient, layer);
}
catch (Exception ex)
{
}
}
}
}
You can also use a gradient stack layout/ Frame as described here with help of custom renderer and use Tap Gesture of it for clicked event.
Gradient Button in Xamarin Forms
Hope this may solve your issue.
I have the following C# class:
class MouseWheelEventListenerViewManager : ReactViewManager
{
public override void OnDropViewInstance(ThemedReactContext reactContext, BorderedCanvas canvas)
{
base.OnDropViewInstance(reactContext, canvas);
RemoveListeners(canvas);
}
protected override BorderedCanvas CreateViewInstance(ThemedReactContext reactContext)
{
var borderedCanvas = base.CreateViewInstance(reactContext);
AddListeners(borderedCanvas);
return borderedCanvas;
}
protected void AddListeners(BorderedCanvas borderedCanvas)
{
Console.Write("In AddListeners");
borderedCanvas.PreviewMouseWheel += PreviewMouseWheel;
}
protected void RemoveListeners(BorderedCanvas borderedCanvas)
{
borderedCanvas.PreviewMouseWheel -= PreviewMouseWheel;
}
protected static void PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
Console.Write("Added PreviewMouseWheel");
if (!e.Handled)
{
e.Handled = true;
var eventArg =
new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
{
RoutedEvent = UIElement.MouseWheelEvent,
Source = sender
};
UIElement parent = VisualTreeHelper.GetParent((UIElement)sender) as UIElement;
parent?.RaiseEvent(eventArg);
}
}
}
This code achieves what it's supposed to do in my application(allow scrolling on top of a ScrollViewer), so I believe it is correct. However, I don't know how to test it.
I added this test which should check that a MouseWheelEvent is marked as handled:
[Test]
public void MouseWheelEvent_is_marked_as_handled()
{
var invoked = new AutoResetEvent(false);
var mouseWheelEventListenerViewManager = new MouseWheelEventListenerViewManager();
BorderedCanvas canvas = mouseWheelEventListenerViewManager.CreateView(new ThemedReactContext(new ReactContext()));
MouseWheelEventArgs me = new MouseWheelEventArgs(InputManager.Current.PrimaryMouseDevice, 10, 10)
{
RoutedEvent = Mouse.MouseWheelEvent
};
canvas.RaiseEvent(me);
Assert.True(invoked.WaitOne(new TimeSpan(0, 0, 0, 20)));
invoked.Set();
Assert.IsTrue(me.Handled);
}
This test fails at Assert.IsTrue(me.Handled)and only this is printed on the console:
In AddListeners
Normally, this should be printed:
In AddListeners
Added PreviewMouseWheel
This means that the method PreviewMouseWheel is not called from the test, even though AddListeners was called, which means that canvas.PreviewMouseWheel was initialised.
Any idea why this happens?
I'm trying to pass an object created in MainWindow to my UserControl that will read and modify it but it doesn't don't know why. Here is the code I'm using:
MainWindow class:
public partial class MainWindow : Window
{
public SupremeLibrary.Player player = new SupremeLibrary.Player();
public MainWindow()
{
InitializeComponent();
MusicSeekBar = new Components.SeekBar(player);
}
}
And SeekBar user control:
public partial class SeekBar : UserControl
{
DispatcherTimer Updater = new DispatcherTimer();
SupremeLibrary.Player player;
/// <summary>
/// Initialize new Seekbar
/// </summary>
public SeekBar()
{
InitializeComponent();
InitializeUpdater();
}
public SeekBar(SupremeLibrary.Player _player)
{
player = _player;
InitializeComponent();
InitializeUpdater();
}
private void InitializeUpdater()
{
Updater.Interval = TimeSpan.FromMilliseconds(100);
Updater.Tick += UpdateSeekBar;
Updater.Start();
}
private void UpdateSeekBar(object sender, EventArgs e)
{
if (player != null)
{
if (player.PlaybackState == SupremeLibrary.PlaybackStates.Playing)
{
if (player.Position.TotalMilliseconds != CustomProgressBar.Value) CustomProgressBar.Value = player.Position.TotalMilliseconds;
if (player.MaxPosition.TotalMilliseconds != CustomProgressBar.Maximum) CustomProgressBar.Maximum = player.MaxPosition.TotalMilliseconds;
}
}
}
private void PB_SeekBar_ChangeValue(object obj, MouseEventArgs e)
{
if (player != null)
{
if (player.PlaybackState == SupremeLibrary.PlaybackStates.Playing)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
player.Position = TimeSpan.FromMilliseconds(e.GetPosition(obj as ProgressBar).X / ((obj as ProgressBar).ActualWidth / 100) * ((obj as ProgressBar).Maximum / 100));
}
}
}
}
In add, it works if I use
public SupremeLibrary.Player player = new SupremeLibrary.Player();
as static and call it in UserControl as MainWindow.player but it's ugly and I don't want to use it.
I have tried to pass player from MainWindow as reference but it doesn't seem to work either.
Example using MediaElement
user control SeekBar
XAML
<UserControl x:Class="CSharpWPF.SeekBar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
DataContext="{Binding RelativeSource={RelativeSource Self}}" >
<Slider Maximum="{Binding TotalMilliseconds}"
Value="{Binding CurrentPosition}"/>
</UserControl>
i have defined a Slider in the control with binding to the maximum and value property to the TotalMilliseconds and CurrentPosition of the control, the properties will be bound to the control itself as I have set the DataContext of the control to self
.cs
public partial class SeekBar : UserControl
{
DispatcherTimer Updater = new DispatcherTimer();
/// <summary>
/// Initialize new Seekbar
/// </summary>
public SeekBar()
{
InitializeComponent();
InitializeUpdater();
}
private void InitializeUpdater()
{
Updater.Interval = TimeSpan.FromMilliseconds(100);
Updater.Tick += UpdateSeekBar;
}
public MediaElement Player
{
get { return (MediaElement)GetValue(PlayerProperty); }
set { SetValue(PlayerProperty, value); }
}
// Using a DependencyProperty as the backing store for Player. This enables animation, styling, binding, etc...
public static readonly DependencyProperty PlayerProperty =
DependencyProperty.Register("Player", typeof(MediaElement), typeof(SeekBar), new PropertyMetadata(null, OnPlayerChanged));
private static void OnPlayerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
SeekBar seekB = d as SeekBar;
if (e.OldValue != null)
{
SeekBar oldSeekB = (e.OldValue as SeekBar);
oldSeekB.Player.MediaOpened -= seekB.Player_MediaOpened;
oldSeekB.Player.MediaEnded -= seekB.Player_MediaEnded;
}
if (seekB.Player != null)
{
seekB.Player.MediaOpened += seekB.Player_MediaOpened;
seekB.Player.MediaEnded += seekB.Player_MediaEnded;
}
}
void Player_MediaEnded(object sender, RoutedEventArgs e)
{
Updater.Stop();
}
private void Player_MediaOpened(object sender, RoutedEventArgs e)
{
if (Player.NaturalDuration.HasTimeSpan)
{
TotalMilliseconds = Player.NaturalDuration.TimeSpan.TotalMilliseconds;
Updater.Start();
}
else
{
CurrentPosition = 0.0;
TotalMilliseconds = 1.0;
}
}
public double CurrentPosition
{
get { return (double)GetValue(CurrentPositionProperty); }
set { SetValue(CurrentPositionProperty, value); }
}
// Using a DependencyProperty as the backing store for CurrentPosition. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CurrentPositionProperty =
DependencyProperty.Register("CurrentPosition", typeof(double), typeof(SeekBar), new PropertyMetadata(1.0, OnCurrentPositionChange));
private static void OnCurrentPositionChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
SeekBar seekB = d as SeekBar;
if (seekB.Player != null)
{
seekB.Player.Position = TimeSpan.FromMilliseconds(seekB.CurrentPosition);
}
}
public double TotalMilliseconds
{
get { return (double)GetValue(TotalMillisecondsProperty); }
set { SetValue(TotalMillisecondsProperty, value); }
}
// Using a DependencyProperty as the backing store for TotalMilliseconds. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TotalMillisecondsProperty =
DependencyProperty.Register("TotalMilliseconds", typeof(double), typeof(SeekBar), new PropertyMetadata(0.0));
private void UpdateSeekBar(object sender, EventArgs e)
{
if (Player != null && TotalMilliseconds > 1)
{
CurrentPosition = Player.Position.TotalMilliseconds;
}
}
}
what I have done
defined property Player of Media Element to be bound at UI
attached MediaOpened and MediaEnded for the purpose of starting and stopping the timer adn updating the duration.
defined properties for current position and total duration for my slider control in the control UI
and on change of CurrentPosition, I'll update back the player's position.
usage in main window
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<MediaElement x:Name="media"
Source="Wildlife.wmv" />
<l:SeekBar Grid.Row="1"
Player="{Binding ElementName=media}" />
</Grid>
I'll just bind the media element to the Player property of my SeekBar control
by doing in this way I've not done any hard coding in code behind, also by means of interface you can achieve a greater decoupling between your seekbar and the player
this is just a simple example for your case, you may use your custom player and progress control in the above example to achieve your results.
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;
}
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
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);
}
}