Bing maps resizable polygon - xaml

I am trying to implement the ability for a user to draw and refine a polygon on a Bing Map control.
I have added a shape layer, and overridden the tap events to draw a simple polygon and that all works perfectly. I would like to further refine the control so that a user can drag and drop any of Location objects of the drawn polygon to alter its shape, but I'm not having any luck.
I have tried to add a MapItemControl, bound to the Locations property of my polygon, but without success - I don't see the pins appear on the map, even though my polygon does render correctly. Presumably the Locations collection isn't notifying the UI of any changes to the collection, so I may have to maintain a separate collection for these pins.
Once I get this going, however, I still need to be able to drag and drop the pins around to modify the polygon. Has anyone implemented this on Windows 8 that could share some thoughts, please?
EDIT
I now have my pins showing the Locations collection on the map. I'd ideally like to tie these back to the Locations collection of the polygon, but beggars can't be choosers. I now just need to be able to drag the pins around.

I added a drag handle module to the polygon in order to edit each point in the polygon. This is the walk-through I used.
Drag Handle Module
This is for version 7.0. Not sure which version you are using, but it is easily adaptable.
Edit I did this in javascript/html. Just noticed you tagged this as windows 8. Curious, are you making a silver-light control for windows 8 phones?
WPF C# Solution
Here is what I came up with and it is very easy to implement, all you need to do is hook my event onto your MapPolygon object. The idea behind the code, is once you click on the polygon, we place a pushpin (which we create events so that we can drag it) at each vertice on the polygon. As we drag and drop each pushpin we adjust the polygon.Locations as applicable.
polygon.MouseDown += new MouseButtonEventHandler(polygon_MouseDownResize);
And a way to exit the resize event, I choose to just hook onto the key down and used 'esc' to stop editing, but you can do this on right click or any other event you choose to. All your doing is clearing pushPinVertices list and resetting event variables.
// === Editable polygon Events ===
private bool _shapeEdit;
public MapPolygon selectedPoly { get; set; }
public List<Pushpin> pushPinVertices { get; set; }
void polygon_MouseDownResize(object sender, MouseButtonEventArgs e)
{
if (!_shapeEdit && selectedPoly == null)
{
_shapeEdit = true;
selectedPoly = sender as MapPolygon;
pushPinVertices = new List<Pushpin>();
int i = 0;
foreach (Microsoft.Maps.MapControl.WPF.Location vertice in selectedPoly.Locations)
{
Pushpin verticeBlock = new Pushpin();
// I use a template to place a 'vertice marker' instead of a pushpin, il provide resource below
verticeBlock.Template = (ControlTemplate)Application.Current.Resources["PushPinTemplate"];
verticeBlock.Content = "vertice";
verticeBlock.Location = vertice;
verticeBlock.MouseDown += new MouseButtonEventHandler(pin_MouseDown);
verticeBlock.MouseUp += new MouseButtonEventHandler(pin_MouseUp);
myMap.Children.Add(verticeBlock);
pushPinVertices.Add(verticeBlock);
i++;
}
}
}
private void myMap_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == System.Windows.Input.Key.Escape)
{
if (_shapeEdit && selectedPoly != null)
{
foreach (Pushpin p in pushPinVertices)
{
myMap.Children.Remove(p);
}
_shapeEdit = false;
selectedPoly = null;
}
}
}
// Note: I needed my window to pass bing maps the keydown event
private void Window_KeyDown(object sender, KeyEventArgs e)
{
myMap_KeyDown(sender, e);
}
// === Editable polygon Events ===
// ==== Draggable pushpin events =====
Vector _mouseToMarker;
private bool _IsPinDragging;
public Pushpin SelectedPushpin { get; set; }
void pin_MouseUp(object sender, MouseButtonEventArgs e)
{
LocationCollection locCol = new LocationCollection();
foreach (Pushpin p in pushPinVertices)
{
locCol.Add(p.Location);
}
selectedPoly.Locations = locCol;
bingMapRefresh();
_IsPinDragging = false;
SelectedPushpin = null;
}
void pin_MouseDown(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
SelectedPushpin = (Pushpin)sender;
_IsPinDragging = true;
_mouseToMarker = Point.Subtract(
myMap.LocationToViewportPoint(SelectedPushpin.Location),
e.GetPosition(myMap));
}
private void myMap_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
if (_IsPinDragging && SelectedPushpin != null)
{
SelectedPushpin.Location = myMap.ViewportPointToLocation(Point.Add(e.GetPosition(myMap), _mouseToMarker));
e.Handled = true;
}
}
}
// ==== Draggable pushpin events =====
// Nice little maprefresh I found online since the bingmap WPF doesnt always seem to update elements after certain event orders
private void bingMapRefresh()
{
//myMap.UpdateLayout();
var mapCenter = myMap.Center;
mapCenter.Latitude += 0.00001;
myMap.SetView(mapCenter, myMap.ZoomLevel);
}
As for the resource to overwrite the pushpin icon, I just used an ImageBrush an made a small white square. Note you may need to adjust the Margin property depending on how long / wide you make your marker. This is because generally the pushpin is anchored above the location by default.
<Application.Resources>
<ControlTemplate x:Key="PushPinTemplate">
<Grid>
<Rectangle Width="10" Height="10" Margin="0 35 0 0">
<Rectangle.Fill>
<ImageBrush ImageSource="pack://application:,,,/Images/DragIcon.gif"/>
</Rectangle.Fill>
</Rectangle>
</Grid>
</ControlTemplate>
</Application.Resources>

Related

Repeating brush or tile of image in WinUI 3

I'm finding it awfully hard to see how to simply cover a rectangular XAML element with repeating copies of a bitmap! I am using WinUI 3 with Windows App SDK. I would like to use the repeating image as a background element in my app.
It would seem to involve the composition API. Some tantalizing clues are given by Deiderik Krohls and by JetChopper ... however (a) there does not seem to be a stable released NuGet package for the required interface and (b) this seems like a very complicated way to do something that should be simple and (c) these solutions would seem to require extra work to integrate with WinUI 3 classes such as ImageSource and BitmapImage.
Any suggestions?
You can use a Direct2D effect, the Tile Effect for that. This effect is hardware accelerated. Microsoft provides a nuget called Win2D that enables that for WinUI: Microsoft.Graphics.Win2D
Once you have created a standard WinUI3 application project, add this nuget, and for this XAML:
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal">
<canvas:CanvasControl
x:Name="myCanvas"
Width="128"
Height="128"
CreateResources="myCanvas_CreateResources"
Draw="myCanvas_Draw" />
</StackPanel>
You can display a repetition of an image with a C# code like this:
public sealed partial class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
}
// handle canvas' CreateResources event for Win2D (Direct2D) resources
private void myCanvas_CreateResources(CanvasControl sender, CanvasCreateResourcesEventArgs args)
=> args.TrackAsyncAction(CreateResources(sender).AsAsyncAction());
// create all needed resources async (here a bitmap)
CanvasBitmap _canvasBitmap;
private async Task CreateResources(CanvasControl sender)
{
// this is my 32x32 image downloaded from https://i.stack.imgur.com/454HU.jpg?s=32&g=1
_canvasBitmap = await CanvasBitmap.LoadAsync(sender, #"c:\downloads\smo.jpg");
}
// handle canvas' Draw event
// check quickstart https://microsoft.github.io/Win2D/WinUI3/html/QuickStart.htm
private void myCanvas_Draw(CanvasControl sender, CanvasDrawEventArgs args)
{
// create an intermediate command list as a feed to the Direct2D effect
using var list = new CanvasCommandList(sender);
using var session = list.CreateDrawingSession();
session.DrawImage(_canvasBitmap);
// create the Direct2D effect (here Tile effect https://learn.microsoft.com/en-us/windows/win32/direct2d/tile)
using var tile = new TileEffect();
tile.Source = list;
// use image size as source rectangle
tile.SourceRectangle = _canvasBitmap.Bounds;
// draw the effect (using bitmap as input)
args.DrawingSession.DrawImage(tile);
}
}
Here is the result with my StackOverflow avatar as the bitmap source:
The image is 32x32 and the canvas is 128x128 so we have 4x4 tiles.
You can use the TilesBrush from the CommunityToolkit.
Install the CommunityToolkit.WinUI.UI.Media NuGet package and try this code:
<Window
x:Class="TileBrushes.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:toolkit="using:CommunityToolkit.WinUI.UI.Media"
mc:Ignorable="d">
<Grid ColumnDefinitions="*,*">
<Border Grid.Column="0">
<TextBlock Text="No tiles" />
</Border>
<Border Grid.Column="1">
<Border.Background>
<toolkit:TilesBrush TextureUri="ms-appx:///Assets/StoreLogo.png" />
</Border.Background>
<TextBlock Text="Tiles" />
</Border>
</Grid>
</Window>
The answer from #simon-mourier was the key for me in finally getting this done.
I created a TiledContentControl that has a ContentControl in front of the tiled background, and which reloads its bitmap image when the TileUriString property is changed (e.g. due to a binding).
There are also properties TileWidth, TileHeight to control the drawn size of the tile bitmap, as well as AlignRight and AlignBottom to make the bitmap align with the right edge or bottom edge instead of the left or top edge. The alignment parameters are useful to get a seamless continuity between two TiledContentControls that are right next to each other.
I am providing this back to the community with thanks for all of the help I've gotten on various coding challenges in the past. Note: I have done some basic testing but not extensive testing.
The key nuget packages used are Microsoft.Graphics.Win2D 1.0.4 and Microsoft.WindowsAppSDK 1.2. There are some interesting coding challenges that I discuss in a comment in the code. For example the need to prevent memory leakage when subscribing to Win2D C++ events from WinUI3 C# code.
Here is TiledContentControl.xaml:
<UserControl
x:Class="Z.Framework.TiledContentControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:win2d="using:Microsoft.Graphics.Canvas.UI.Xaml"
mc:Ignorable="d"
Padding="0"
>
<Grid
RowDefinitions="*"
ColumnDefinitions="*"
>
<win2d:CanvasControl
x:Name="CanvasControl"
Grid.Row="0"
Grid.Column="0"
>
</win2d:CanvasControl>
<ContentPresenter
Name="ContentPresenter"
Grid.Row="0"
Grid.Column="0"
Background="Transparent"
Foreground="{x:Bind Foreground, Mode=OneWay}"
HorizontalContentAlignment="{x:Bind HorizontalContentAlignment, Mode=OneWay}"
VerticalContentAlignment="{x:Bind VerticalContentAlignment, Mode=OneWay}"
Padding="{x:Bind Padding, Mode=OneWay}"
Content="{x:Bind Content, Mode=OneWay}"
>
</ContentPresenter>
</Grid>
</UserControl>
Here is TiledContentControl.xaml.cs:
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Brushes;
using Microsoft.Graphics.Canvas.UI;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Markup;
using System;
using System.Diagnostics;
using System.Numerics;
using System.Threading.Tasks;
using Windows.Foundation;
namespace Z.Framework
{
/// <summary>
/// A control that has a tiled (repeating) bitmap background behind a content control.
///
/// Setting the TileUriString will change the tiled bitmap. Setting the drawing parameters
/// (TileWidth, TileHeight, AlignRight, AlignBottom) will scale the bitmap or offset it so
/// that it is right or bottom aligned.
/// </summary>
[ContentProperty(Name="Content")]
public sealed partial class TiledContentControl : UserControl
{
#region Discussion
// There are a number of necessary objectives to achieve the Win2D tiling with post-Load updates.
// Goal: to trigger an async load-resources when a resource-related property of the control
// changes. This is accomplished by calling StartLoadingResources when the TileUriString changes.
// Goal: cancel any resource loads that are in progress when the new load is requested.
// This is done in StartNewLoadResourcesTaskAndCleanupOldTaskAsync.
// To do it, one must store the resource-loading task (LoadResourcesTask).
// Goal: to store the resources that have been loaded, and dispose them timely.
// The LoadResourcesTask contains the loaded resources in the Result property.
// They are kept around indefinitely, except if we start a new resource load task
// then any resources in the old load task are disposed. Also, when loading several
// resources, if one of the resource loads fails then we dispose of the others.
// The CanvasResourcesRecord and LoadResourcesAsync provide a generalizable way of
// storing resources in the task result.
// Goal: make sure that any exceptions from resource creation are thrown to Win2D, so that
// Win2D can handle device-lost events (which includes Win2D triggering a new CreateResources).
// It is accomplished by only throwing load-resource exceptions from the Win2d draw handler.
// Goal: prevent Draw from being called before resources are loaded. Resource loads that are
// triggered by Win2D go through the CreateResources event handler, allowing the use of
// CanvasCreateResourcesEventArgs.TrackAsyncAction which will postpone the Draw call -- not
// until the resources are loaded but at least while the load task is started. A Draw
// callback may then occur before the load completes, but then when the load completes
// it will invalidate the CanvasControl and another Draw callback will occur.
// It does not appear to be necessary from a Win2D perspective to prevent Draw calls
// while subsequent (post-CreateResources) resource loads are being done.
// Goal: to prevent memory leaks due to .NET not being able to detect the reference cycle
// between the main control and the CanvasControl. This is accomplished by only subscribing
// to CanvasControl events while the main control is loaded.
// References:
// https://microsoft.github.io/Win2D/WinUI2/html/M_Microsoft_Graphics_Canvas_UI_CanvasCreateResourcesEventArgs_TrackAsyncAction.htm
// https://stackoverflow.com/questions/74527783/repeating-brush-or-tile-of-image-in-winui-3-composition-api
// https://microsoft.github.io/Win2D/WinUI2/html/RefCycles.htm
// https://english.r2d2rigo.es/
// https://microsoft.github.io/Win2D/WinUI3/html/M_Microsoft_Graphics_Canvas_UI_CanvasCreateResourcesEventArgs_TrackAsyncAction.htm
// https://learn.microsoft.com/en-us/windows/win32/direct2d/tile
#endregion
#region ctor
public TiledContentControl()
{
this.InitializeComponent();
this.Loaded += this.TiledContentControl_Loaded; // OK, same lifetime
this.Unloaded += this.TiledContentControl_Unloaded; // OK, same lifetime
}
private void TiledContentControl_Loaded(object sender, RoutedEventArgs e)
{
this.CanvasControl.Draw += this.CanvasControl_Draw; // OK, matched in Unloaded
this.CanvasControl.CreateResources += this.CanvasControl_CreateResources;
}
private void TiledContentControl_Unloaded(object sender, RoutedEventArgs e)
{
this.CanvasControl.Draw -= this.CanvasControl_Draw;
this.CanvasControl.CreateResources -= this.CanvasControl_CreateResources;
}
#endregion
#region CanvasResourcesRecord, LoadResourcesAsync, LoadResourcesTask
private record class CanvasResourcesRecord(
CanvasBitmap TileBitmap,
CanvasImageBrush TileBrush
): IDisposable
{
public void Dispose()
{
this.TileBitmap.Dispose();
this.TileBrush.Dispose();
}
}
static private async Task<CanvasResourcesRecord> LoadResourcesAsync(CanvasControl canvasControl, string tileUriString)
{
object[] resources = new object[2];
try {
Uri tileUri = new Uri(tileUriString);
Task<CanvasBitmap> loadTileBitmap = CanvasBitmap.LoadAsync(canvasControl, tileUri).AsTask();
CanvasBitmap tileBitmap = await loadTileBitmap;
resources[0] = tileBitmap;
CanvasImageBrush tileBrush = new CanvasImageBrush(canvasControl, tileBitmap);
tileBrush.ExtendX = CanvasEdgeBehavior.Wrap;
tileBrush.ExtendY = CanvasEdgeBehavior.Wrap;
resources[1] = tileBrush;
} catch {
// Cleanup from partial/incomplete creation
foreach (object? resource in resources) {
(resource as IDisposable)?.Dispose();
}
throw;
}
canvasControl.Invalidate(); // now that resources are loaded, we trigger an async Draw.
return new CanvasResourcesRecord(
TileBitmap: (CanvasBitmap)resources[0],
TileBrush: (CanvasImageBrush)resources[1]
);
}
private Task<CanvasResourcesRecord>? LoadResourcesTask {
get { return this._loadResourcesTask; }
set { this._loadResourcesTask = value; }
}
private Task<CanvasResourcesRecord>? _loadResourcesTask;
#endregion
#region CanvasControl_CreateResources
private void CanvasControl_CreateResources(CanvasControl sender, CanvasCreateResourcesEventArgs args)
{
Debug.Assert(sender == this.CanvasControl);
args.TrackAsyncAction(this.StartNewLoadResourcesTaskAndCleanupOldTaskAsync().AsAsyncAction());
}
#endregion
#region StartLoadingResources, StartNewLoadResourcesTaskAndCleanupOldTaskAsync
private void StartLoadingResources()
{
if (this.CanvasControl.IsLoaded) {
Task _ = this.StartNewLoadResourcesTaskAndCleanupOldTaskAsync();
}
}
private async Task StartNewLoadResourcesTaskAndCleanupOldTaskAsync()
{
// Start new task, if the necessary input properties are available.
string? tileUriString = this.TileUriString;
Task<CanvasResourcesRecord>? oldTask = this.LoadResourcesTask;
if (tileUriString != null) {
this.LoadResourcesTask = LoadResourcesAsync(this.CanvasControl, tileUriString);
} else {
this.LoadResourcesTask = null;
}
// Cleanup old task.
if (oldTask != null) {
oldTask.AsAsyncAction().Cancel();
try {
await oldTask;
} catch {
// ignore exceptions from the cancelled task
} finally {
if (oldTask.IsCompletedSuccessfully) {
oldTask.Result.Dispose();
}
}
}
}
#endregion
#region CanvasControl_Draw, ActuallyDraw
private void CanvasControl_Draw(CanvasControl sender, CanvasDrawEventArgs args)
{
Debug.Assert(sender == this.CanvasControl);
if (!this.DrawingParameters.AreFullyDefined) { return; }
if (!this.DrawingParameters.AreValid) { throw new InvalidOperationException($"Invalid drawing parameters (typically width or height)."); }
Task<CanvasResourcesRecord>? loadResourcesTask = this.LoadResourcesTask;
if (loadResourcesTask == null) { return; }
if (loadResourcesTask.IsCompletedSuccessfully) {
CanvasResourcesRecord canvasResources = loadResourcesTask.Result;
this.ActuallyDraw( args, canvasResources);
} else if (loadResourcesTask.IsFaulted) {
// Throw exceptions to Win2D, for example DeviceLostException resulting in new CreateResoures event
loadResourcesTask.Exception?.Handle(e => throw e);
} else {
return;
}
}
private void ActuallyDraw( CanvasDrawEventArgs args, CanvasResourcesRecord canvasResources)
{
Debug.Assert(this.DrawingParameters.AreFullyDefined && this.DrawingParameters.AreValid);
Debug.Assert(this.DrawingParameters.AlignRight != null && this.DrawingParameters.AlignBottom != null);
CanvasControl canvasControl = this.CanvasControl;
float scaleX = (float)(this.DrawingParameters.TileWidth / canvasResources.TileBitmap.Bounds.Width);
float scaleY = (float)(this.DrawingParameters.TileHeight / canvasResources.TileBitmap.Bounds.Height);
float translateX = ((bool)this.DrawingParameters.AlignRight) ? (float)((canvasControl.RenderSize.Width % this.DrawingParameters.TileWidth) - this.DrawingParameters.TileWidth) : (float)0;
float translateY = ((bool)this.DrawingParameters.AlignBottom) ? (float)((canvasControl.RenderSize.Height % this.DrawingParameters.TileHeight) - this.DrawingParameters.TileHeight) : (float)0;
Matrix3x2 transform = Matrix3x2.CreateScale( scaleX, scaleY);
transform.Translation = new Vector2(translateX, translateY);
canvasResources.TileBrush.Transform = transform;
Rect rectangle = new Rect(new Point(), canvasControl.RenderSize);
args.DrawingSession.FillRectangle(rectangle, canvasResources.TileBrush);
}
#endregion
#region Content
new public UIElement? Content {
get { return (UIElement?)this.GetValue(ContentProperty); }
set { this.SetValue(ContentProperty, value); }
}
new public static DependencyProperty ContentProperty { get; } = DependencyProperty.Register(nameof(TiledContentControl.Content), typeof(UIElement), typeof(TiledContentControl), new PropertyMetadata(default(UIElement)));
#endregion
#region TileUriString
public string? TileUriString {
get { return (string?)this.GetValue(TileUriStringProperty); }
set { this.SetValue(TileUriStringProperty, value); }
}
public static readonly DependencyProperty TileUriStringProperty = DependencyProperty.Register(nameof(TiledContentControl.TileUriString), typeof(string), typeof(TiledContentControl), new PropertyMetadata(default(string), new PropertyChangedCallback(OnTileUriStringChanged)));
private static void OnTileUriStringChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
TiledContentControl #this = (TiledContentControl)sender;
#this.StartLoadingResources();
}
#endregion
#region TileWidth, TileHeight, AlignRight, AlignBottom; OnDrawingParameterChanged, DrawingParametersRecord, DrawingParameters
public double TileWidth {
get { return (double)this.GetValue(TileWidthProperty); }
set { this.SetValue(TileWidthProperty, value); }
}
public static readonly DependencyProperty TileWidthProperty = DependencyProperty.Register(nameof(TileWidth), typeof(double), typeof(TiledContentControl), new PropertyMetadata(double.NaN, new PropertyChangedCallback(OnDrawingParameterChanged)));
public double TileHeight {
get { return (double)this.GetValue(TileHeightProperty); }
set { this.SetValue(TileHeightProperty, value); }
}
public static readonly DependencyProperty TileHeightProperty = DependencyProperty.Register(nameof(TileHeight), typeof(double), typeof(TiledContentControl), new PropertyMetadata(double.NaN, new PropertyChangedCallback(OnDrawingParameterChanged)));
public bool? AlignRight {
get { return (bool?)this.GetValue(AlignRightProperty); }
set { this.SetValue(AlignRightProperty, value); }
}
public static readonly DependencyProperty AlignRightProperty = DependencyProperty.Register(nameof(AlignRight), typeof(bool?), typeof(TiledContentControl), new PropertyMetadata(default(bool?), new PropertyChangedCallback(OnDrawingParameterChanged)));
public bool? AlignBottom {
get { return (bool?)this.GetValue(AlignBottomProperty); }
set { this.SetValue(AlignBottomProperty, value); }
}
public static readonly DependencyProperty AlignBottomProperty = DependencyProperty.Register(nameof(AlignBottom), typeof(bool?), typeof(TiledContentControl), new PropertyMetadata(default(bool?), new PropertyChangedCallback(OnDrawingParameterChanged)));
private static void OnDrawingParameterChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
TiledContentControl #this = (TiledContentControl)sender;
#this.DrawingParameters = new DrawingParametersRecord(#this.TileWidth, #this.TileHeight, #this.AlignRight, #this.AlignBottom);
#this.CanvasControl.Invalidate(); // trigger an async redraw using the new parameters.
}
private record struct DrawingParametersRecord(
double TileWidth,
double TileHeight,
bool? AlignRight,
bool? AlignBottom
)
{
public bool AreFullyDefined => !double.IsNaN(this.TileWidth) && !double.IsNaN(this.TileHeight) && this.AlignBottom != null && this.AlignRight != null;
public bool AreValid => this.TileWidth > 0 && this.TileHeight > 0;
}
private DrawingParametersRecord DrawingParameters { get; set; }
#endregion
}
}

MasterDetailView UWP - Access the ListView (or make it scroll)

I recently discovered the UWP Community Toolkit and I loved it. I'm using now the MasterDetailView and it's almost perfect for what I need: but it's missing one important thing: I cannot access the internal "ListView".
In my application I have two buttons: forward and back, and when pressing them I simply go forward and back in the list. I found a trick to access the prev/next element doing like this:
public void GoForward()
{
if (MasterDetailView.Items.Count > 0)
{
bool isNextGood = false;
MyItem selectedItem = MasterDetailView.Items[MasterDetailView.Items.Count - 1] as MyItem;
foreach (var v in MasterDetailView.Items)
{
if (isNextGood)
{
selectedItem = v as MyItem;
break;
}
if (v == MasterDetailView.SelectedItem)
isNextGood = true;
}
MasterDetailView.SelectedItem = selectedItem;
}
}
This just because I cannot access the "SelectedIndex" and I have only SelectedItem avaiable. Now, obviously not all the item can be visible at the same time, so MasterDetailView provide a lateral ListView with a scrollbar. When pressing my next/prev buttons SelectedItem changes, but doesn't scroll at the selected element: selection goes forward/back but the list is locked. This produce a very negative feedback because I lost my selection somewhere in the list and I must search it.
How I though to solve it? I try this approaches:
1) Find the style for MasterDetailView. Inside I found a ListViewStyle, so I tried to put inside a simple "SelectionChanged" event and handle it at App.xaml.cs.
<ListView x:Name="MasterList"
Grid.Row="1"
IsTabStop="False"
ItemContainerStyle="{TemplateBinding ItemContainerStyle}"
ItemTemplate="{TemplateBinding ItemTemplate}"
ItemTemplateSelector="{TemplateBinding ItemTemplateSelector}"
ItemsSource="{TemplateBinding ItemsSource}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
SelectionChanged="MasterList_SelectionChanged"/>
CS:
private void MasterList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ListView list = sender as ListView;
list.ScrollIntoView(list.SelectedItem);
}
But, as said, "Events cannot be set in the Application class XAML file".
2) Think about taking the parent of SelectedItem: I tried to convert the SelectedItem in ListViewItem, then access the parent, but it fails at first conversion, as the SelectedItem seems no to be a ListViewItem, but it's of the "MyItem" type. Like this, I cannot access the parent.
private void MasterDetailView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var item = MasterDetailView.SelectedItem as ListViewItem;
var parent = item.Parent;
var list = parent as ListView;
}
And so I'm here... I don't want to throw away all my work with the MasterDetailView to pass to another control. Is there any simple method to access the list, or simply, scroll the list when I'm changing selection? Just wanna to do one thing, like this:
private void List_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ListView list = sender as ListView;
list.ScrollIntoView(list.SelectedItem);
}
Simply scrolling into selection when selection changed occurred, but I have no simple ListView but a MasterDetailView control. Even if it's done entirely in XAML: the most important thing for me is make scroll this list!
Thanks.
 
 
Solution
This method is fantastic. Just copy-paste.
public static T FindChildOfType<T>(DependencyObject root) where T : class
{
var queue = new Queue<DependencyObject>();
queue.Enqueue(root);
while (queue.Count > 0)
{
DependencyObject current = queue.Dequeue();
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(current); i++)
{
var child = VisualTreeHelper.GetChild(current, i);
var typedChild = child as T;
if (typedChild != null)
{
return typedChild;
}
queue.Enqueue(child);
}
}
return null;
}
Then simply use it to retrive the list and make it scroll. Yes babe!
private void MasterDetailView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var v = FindChildOfType<ListView>(MasterDetailView);
v.ScrollIntoView(MasterDetailView.SelectedItem);
}
Please note that "MasterDerailView" Is the x:Name of my element, not the class.
You can use a FindChildOfType implementation to get the ListView through VisualTreeHelper, as indicated in this answer:
Windows UWP - How to programmatically scroll ListView in ContentTemplate

how to set xaml mapcontrol mapicon always visible

I'm fairly new to programming in XAML and I'm making a test application on windows phone 8.1 emulator with a MapControl.
I wanted to add a MapIcon to my map but the icon doesn't appear when the map is zoomed out. I've searched the internet and couldn't find anything regarding my problem.
I want my zoomlevel 12 and show the mapicon on that zoomlevel.
namespace TEST.APPLICATION
{
public partial class MapView : Page
{
Geolocator geo = null;
public MapView()
{
this.InitializeComponent();
this.NavigationCacheMode = NavigationCacheMode.Required;
HardwareButtons.BackPressed += HardwareButtons_BackPressed;
}
void HardwareButtons_BackPressed(object sender, BackPressedEventArgs e)
{
if (Frame.CanGoBack)
{
e.Handled = true;
Frame.GoBack();
}
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
map.Center = new Geopoint(new BasicGeoposition()
{
Latitude = 51.5856935784736,
Longitude = 4.79448171225132
});
map.ZoomLevel = 12;
displaySightings();
}
private void displaySightings()
{
MapIcon sighting1 = new MapIcon();
sighting1.Location = new Geopoint(new BasicGeoposition()
{
Latitude = 51.5940,
Longitude = 4.7795
});
//sighting1.NormalizedAnchorPoint = new Point(0.5, 1.0);
sighting1.Title = "VVV";
map.MapElements.Add(sighting1);
}
}
Is there any way to make the MapIcon always visible?
The MapIcon is not guaranteed to be shown. It may be hidden when it obscures other elements or labels on the map.
For some stupid reason, Microsoft thought that labels and other map elements should outrank map icons when rendering the display. So, if you're making an app displaying the locations of all the nearby Starbucks, the name of the high school across the street from the Starbucks is more important than the pushpin, according to them.
You'll need to render the pushpins using XAML instead.

Value of property binded to a TextBlock inside a User Control is not being detected

I've created a UserControl, LiveTile.xaml (streamlined for brevity):
<UserControl
x:Class="Weathercast.Core.LiveTile"
xmlns:local="clr-namespace:Weathercast.Core">
<StackPanel
x:Name="LayoutRoot">
<StackPanel
x:Name="TileRegularFront"
Width="336"
Height="336"
Background="Red">
<TextBlock Text="{Binding TempCurrentHour}"/>
</StackPanel>
</UserControl>
Its code behind, LiveTile.xaml.cs:
public partial class LiveTile : UserControl
{
public LiveTile()
{
InitializeComponent();
LiveTileViewModel vm = new LiveTileViewModel();
this.DataContext = vm;
}
}
Its view model, LiveTileViewModel.cs:
public class LiveTileViewModel : ObservableObject
{
/** PROPERTIES **/
private string _tempCurrentHour;
public string TempCurrentHour
{
get { return _tempCurrentHour; }
set
{
_tempCurrentHour = value;
RaisePropertyChanged("TempCurrentHour");
}
}
/** CONSTRUCTOR **/
public LiveTileViewModel()
{
this.TempCurrentHour = "15"; // dummy value set
}
}
ObservableObject.cs:
public abstract class ObservableObject : INotifyPropertyChanged, INotifyPropertyChanging
{
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
The problem is the value I'm binding ("TempCurrentHour") is not being displayed. Any ideas on what I need to do in order to get the User Control's View Model's binded property value to display? Based on my research, I believe binding a value to a User Control is less straightforward than normal. However I can't get my head around what needs to be done to get the User Control to detect binded property values.
UPDATE: Just to be clear, the LiveTile class is in a Library project for my solution. An instance of it is created when the user toggles on the Live Tile via the Settings PhoneApplicationPage located in the Windows Phone App project in the solution. This is the event handler that instantiates a LiveTile in Settings.xaml.cs:
private void LiveTile_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Weathercast.Core.LiveTile l = new Weathercast.Core.LiveTile();
l.CreateOrUpdateTile(1);
}
The CreateOrUpdateTile method is doing its job correctly and takes the user back to their phone's Start screen with the Live Tile now there. This is its code in any case (I'm using Telerik's LiveTileHelper):
RadFlipTileData tileData = new RadFlipTileData()
{
VisualElement = this.TileRegularFront,
BackVisualElement = this.TileRegularBack,
SmallVisualElement = this.TileSmall
};
// Tile's uri has a unique paramater which is the location Id of the currently viewed location.
Uri tileUri = new Uri("/MainPage.xaml?locationId=" + locationId, UriKind.RelativeOrAbsolute);
// If the tile for this location previously existed, delete it before adding it anew.
ShellTile tile = LiveTileHelper.GetTile(tileUri);
if (tile != null)
{
tile.Delete();
}
// Create brand new tile for location if didn't have tile previously or fresh tile if it did.
LiveTileHelper.CreateOrUpdateTile(tileData, tileUri, true);
this.tile = tileData;
// Add the Background Agent for this tile with the agent's name
// unique for the location.
AddAgent("PeriodicTaskForLocation" + locationId);
I should note another problem I'm having, that may or may not be related to the original issue, is that the background property I'm setting in LiveTile.xaml for the StackPanel or LayoutRoot element even is being neglected and the Live Tile that's being added to the Start screen is transparent (black).

XAML: Tap a textbox to enable?

In XAML and WinRT, Is there a way to set up a textbox so that it is disabled for text input until it is tapped.
I tried setting up the Tapped event and then setting the IsEnabled=true, but that only seems to work if the IsEnabled=true in the first place.
I found this on MSDN:
http://social.msdn.microsoft.com/Forums/en-US/winappswithcsharp/thread/708c0949-8b06-40ec-85fd-201139ca8b2d
Talks about adding the TappedEvent manually to the event handled for each TextBox, which is cumbersome, but also doesn't seem to work unless IsEnabled was already set to true.
Basically, I want a form where all textboxes display data but are disabled unless the user taps to enable the box and then type.
You can use IsReadOnly instead of IsEnabled to achieve what you are looking for. In addition, you can set up the tapped event handlers in code easily. I'm not sure if setting up handlers in code is a requirement for this to work, as you noted above; however, it does simplify things.
Here are the details.
In the constructor of your page class (here it is MainPage), call the setup function:
public MainPage()
{
this.InitializeComponent();
// call the setup for the textboxes
SetupTextBoxes();
}
Here is where we do the magic - make all textboxes on this page readonly and set up tap handler:
private void SetupTextBoxes()
{
var tbs = GetVisualChildren<TextBox>(this, true);
foreach (var tb in tbs)
{
tb.IsReadOnly = true;
tb.AddHandler(TappedEvent, new TappedEventHandler(tb_Tapped), true);
}
}
Utility function to get a list of all children of the given type (T) of the passed in parent.
private List<T> GetVisualChildren<T>(DependencyObject parent, bool recurse = true)
where T : DependencyObject
{
var children = new List<T>();
int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < numVisuals; i++)
{
DependencyObject v = (DependencyObject)VisualTreeHelper.GetChild(parent, i);
var child = v as T;
if (child == null && recurse)
{
var myChildren = GetVisualChildren<T>(v, recurse);
children.AddRange(myChildren);
}
if (child != null)
children.Add(child);
}
return children;
}
Finally, the event handler. This enables each textbox when tapped.
private void tb_Tapped(object sender, TappedRoutedEventArgs e)
{
((TextBox)(sender)).IsReadOnly = false;
}