How to make MarkupExtension visible in Xaml - xamarin.forms

I am working on a project using Xamarin.Forms.
I have a MarkupExtension for text translations defined in PCL which is
public (so should be visible outside the PCL) and
there is also a static class for PCL initialization and there is a call to its Init method in App contructor
Extension code:
[ContentProperty("Text")]
public class TranslateExtension : IMarkupExtension
{
readonly CultureInfo ci;
const string ResourceId = "GymHeroViews.Resources.AppResources";
public TranslateExtension()
{
ci = DependencyService.Get<ILocalize>().GetCurrentCultureInfo();
}
public string Text { get; set; }
public object ProvideValue(IServiceProvider serviceProvider)
{
if (Text == null)
return "";
ResourceManager resmgr = new ResourceManager(ResourceId
, typeof(TranslateExtension).GetTypeInfo().Assembly);
var translation = resmgr.GetString(Text, ci);
if (translation == null)
{
#if DEBUG
throw new ArgumentException(
String.Format("Key '{0}' was not found in resources '{1}' for culture '{2}'.", Text, ResourceId, ci.Name),
"Text");
#else
translation = Text;
#endif
}
return translation;
}
}
Extension is included in GymHero.Common namespace. And output dll's name is GymHero.Common.dll.
In Xaml file I have defined a namespace:
xmlns:t="clr-namespace:GymHero.Common;assembly:GymHero.Common"
Then I have defined a Button:
<Button x:Name="Excersise1GridButton" Text="{t:Translate Excersise1GridButton}" Grid.Row="0" Grid.Column="1" />
For me, everything looks as it should but I get the exception:
Xamarin.Forms.Xaml.XamlParseException: Type TranslateExtension not
found in xmlns clr-namespace:GymHero.Common;assembly:GymHero.Common
I would appreciate any help. Thanks in advance.
Tomasz

Related

Xamarin Forms 5.0.0.2515: Relative routing to shell elements is currently not supported

I am receiving the below error :
Relative routing to shell elements is currently not supported. I am running Xamarin forms 5.0.0.2515.
The project navigates to the Items Journal Template View Model where I select an item. The item's name is then passed to the Finished Goods View Model where I will do a search based on the value passed. However I am receiving the above error even though I have already done this on other pages without an error.
I have my routing registered:
public AppShell()
{
InitializeComponent();
Routing.RegisterRoute(nameof(LocatePage), typeof(LocatePage));
Routing.RegisterRoute(nameof(FinishedGoodsPage), typeof(FinishedGoodsPage));
Routing.RegisterRoute(nameof(ItemJournalTemplatePage), typeof(ItemJournalTemplatePage));
}
ItemJournalTemplateViewModel: I call the Finished Goods page and pass it the item's name...
async void SelectedItemJournalTemplate(ItemJournalTemplate item)
{
if (item == null)
return;
await Shell.Current.GoToAsync($"{nameof(FinishedGoodsPage)}?{nameof(FinishedGoodsViewModel.PassedJournalBatchName)}={item.Name}");
}
and the page receiving the call FinishedGoodsViewModel:
[QueryProperty(nameof(PassedJournalBatchName), nameof(PassedJournalBatchName))]
public class FinishedGoodsViewModel:BaseViewModel
{
private string passedJournalBatchName;
public string PassedJournalBatchName
{
get => passedJournalBatchName;
set
{
passedJournalBatchName = value;
OnPropertyChanged(nameof(PassedJournalBatchName));
}
} .....
}
What makes this odd is that I use this on the FinishedGoodsViewModel to call the LocateViewModel and it works fine:
public async void UpdateLocation(object value)
{
SelectedValue = (FinishedGood)value;
await Shell.Current.GoToAsync($"{nameof(LocatePage)}" +
$"?{nameof(LocateViewModel.ItemNo)}={SelectedValue.Item_No}" +
$"&{nameof(LocateViewModel.BatchName)}={SelectedValue.Journal_Batch_Name}" +
$"&{nameof(LocateViewModel.TemplateName)}={SelectedValue.Journal_Template_Name}" +
$"&{nameof(LocateViewModel.LineNo)}={SelectedValue.Line_No}");
}
LocateViewModel:
[QueryProperty(nameof(ItemNo), nameof(ItemNo))]
[QueryProperty(nameof(BatchName), nameof(BatchName))]
[QueryProperty(nameof(TemplateName), nameof(TemplateName))]
[QueryProperty(nameof(LineNo), nameof(LineNo))]
public class LocateViewModel : BaseViewModel
{
private string batchName;
public string BatchName
{ get => batchName;
set
{
batchName = value;
OnPropertyChanged(nameof(BatchName));
}
}
private string templateName;
public string TemplateName
{
get => templateName;
set
{
templateName = value;
OnPropertyChanged(nameof(TemplateName));
}
}
private string lineNo;
public string LineNo
{
get => lineNo;
set
{
lineNo = value;
OnPropertyChanged(nameof(LineNo));
}
}
private string itemNo;
public string ItemNo
{
get => itemNo;
set
{
itemNo = value;
OnPropertyChanged(nameof(ItemNo));
}
}...
}
Can someone please tell me where I am going wrong here?
It looks like the answer was in the AppShell.xaml file. The original Flyout pointed to the FinsishedGoodsPage but I changed it to ItemJournalTemplatePage, allowing the user to first make a selection first.
<FlyoutItem Title="Finished Goods" Icon="placeholder.png">
<ShellContent Route="FinishedGoodsPage" ContentTemplate="{DataTemplate local:ItemJournalTemplatePage}" />
</FlyoutItem>
I forgot I also needed to change the Route. It probably would not go to the FinishedGoodsPage because it thought it was already on that page.
<FlyoutItem Title="Finished Goods" Icon="placeholder.png">
<ShellContent Route="ItemJournalTemplatePage" ContentTemplate="{DataTemplate local:ItemJournalTemplatePage}" />
</FlyoutItem>

Validating forms only on submit with Blazor

I've recently started using Blazor. Is there a way to trigger form model validation only on submit, instead of live on each change?
Just for clarification, let's say I have something like this:
<EditForm Model="this" OnValidSubmit="SubmitForm">
<DataAnnotationsValidator />
<ValidationSummary />
<Label For="Name">Name</Label>
<InputText id="Name" name="Name" class="form-control" #bind-Value="Name"/>
<button type="submit">Save</button>
</EditForm>
#code {
[StringLength(10, ErrorMessage="Name too long")]
public string Name { get; set; }
private async Task SubmitForm()
{
// ...
// send a POST request
}
}
By default, it seems like the validity of the field and the error messages displayed in the ValidationSummary get re-evaluated on every change of the text input (e.g. as soon as I delete the 11th character from the input, the "too long" message disappears).
I would prefer if the displayed messages would remain frozen until the Submit button is clicked.
I suppose it would be possible to implement it by removing the ValidationSummary component and implementing a custom solution (e.g. displaying a List of error messages that's refreshed only on submit), but I was wondering if there is some idiomatic solution that I'm not aware of.
When validation occurs is controlled by the Validator you're using.
There are two events that you can receive from EditContext:
OnValidationRequested is invoked either when EditContext.Validate is called or as part of the form submission process.
OnFieldChanged is invoked every time a field value is changed.
A validator uses these events to trigger it's validation process, and outputs the results to the EditContext's ValidationMessageStore.
DataAnnotationsValidator wires up for both events and triggers validation whenever either is invoked.
There are other validators out there, and writing your own is not too difficult. Other than those from the usual control suppliers, there's Blazored, or mine. Mine is documented here - https://shauncurtis.github.io/articles/Blazor-Form-Validation.html. it has a DoValidationOnFieldChange setting!
#enet's answer sparked an alternative answer. Build your own DataAnnotationsValidator.
Here's the EditContext Extensions code. It's a modified version of the original MS Code with some extra control arguments.
using Microsoft.AspNetCore.Components.Forms;
using System.Collections.Concurrent;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Reflection.Metadata;
using System.Runtime.InteropServices;
namespace StackOverflowAnswers;
public static class EditContextCustomValidationExtensions
{
public static IDisposable EnableCustomValidation(this EditContext editContext, bool doFieldValidation, bool clearMessageStore)
=> new DataAnnotationsEventSubscriptions(editContext, doFieldValidation, clearMessageStore);
private static event Action? OnClearCache;
private static void ClearCache(Type[]? _)
=> OnClearCache?.Invoke();
private sealed class DataAnnotationsEventSubscriptions : IDisposable
{
private static readonly ConcurrentDictionary<(Type ModelType, string FieldName), PropertyInfo?> _propertyInfoCache = new();
private readonly EditContext _editContext;
private readonly ValidationMessageStore _messages;
private bool _doFieldValidation;
private bool _clearMessageStore;
public DataAnnotationsEventSubscriptions(EditContext editContext, bool doFieldValidation, bool clearMessageStore)
{
_doFieldValidation = doFieldValidation;
_clearMessageStore = clearMessageStore;
_editContext = editContext ?? throw new ArgumentNullException(nameof(editContext));
_messages = new ValidationMessageStore(_editContext);
if (doFieldValidation)
_editContext.OnFieldChanged += OnFieldChanged;
_editContext.OnValidationRequested += OnValidationRequested;
if (MetadataUpdater.IsSupported)
{
OnClearCache += ClearCache;
}
}
private void OnFieldChanged(object? sender, FieldChangedEventArgs eventArgs)
{
var fieldIdentifier = eventArgs.FieldIdentifier;
if (TryGetValidatableProperty(fieldIdentifier, out var propertyInfo))
{
var propertyValue = propertyInfo.GetValue(fieldIdentifier.Model);
var validationContext = new ValidationContext(fieldIdentifier.Model)
{
MemberName = propertyInfo.Name
};
var results = new List<ValidationResult>();
Validator.TryValidateProperty(propertyValue, validationContext, results);
_messages.Clear(fieldIdentifier);
foreach (var result in CollectionsMarshal.AsSpan(results))
{
_messages.Add(fieldIdentifier, result.ErrorMessage!);
}
// We have to notify even if there were no messages before and are still no messages now,
// because the "state" that changed might be the completion of some async validation task
_editContext.NotifyValidationStateChanged();
}
}
private void OnValidationRequested(object? sender, ValidationRequestedEventArgs e)
{
var validationContext = new ValidationContext(_editContext.Model);
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(_editContext.Model, validationContext, validationResults, true);
// Transfer results to the ValidationMessageStore
_messages.Clear();
foreach (var validationResult in validationResults)
{
if (validationResult == null)
{
continue;
}
var hasMemberNames = false;
foreach (var memberName in validationResult.MemberNames)
{
hasMemberNames = true;
_messages.Add(_editContext.Field(memberName), validationResult.ErrorMessage!);
}
if (!hasMemberNames)
{
_messages.Add(new FieldIdentifier(_editContext.Model, fieldName: string.Empty), validationResult.ErrorMessage!);
}
}
_editContext.NotifyValidationStateChanged();
}
public void Dispose()
{
if (_clearMessageStore)
_messages.Clear();
if (_doFieldValidation)
_editContext.OnFieldChanged -= OnFieldChanged;
_editContext.OnValidationRequested -= OnValidationRequested;
_editContext.NotifyValidationStateChanged();
if (MetadataUpdater.IsSupported)
{
OnClearCache -= ClearCache;
}
}
private static bool TryGetValidatableProperty(in FieldIdentifier fieldIdentifier, [NotNullWhen(true)] out PropertyInfo? propertyInfo)
{
var cacheKey = (ModelType: fieldIdentifier.Model.GetType(), fieldIdentifier.FieldName);
if (!_propertyInfoCache.TryGetValue(cacheKey, out propertyInfo))
{
// DataAnnotations only validates public properties, so that's all we'll look for
// If we can't find it, cache 'null' so we don't have to try again next time
propertyInfo = cacheKey.ModelType.GetProperty(cacheKey.FieldName);
// No need to lock, because it doesn't matter if we write the same value twice
_propertyInfoCache[cacheKey] = propertyInfo;
}
return propertyInfo != null;
}
internal void ClearCache()
=> _propertyInfoCache.Clear();
}
}
And the CustomValidation component:
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
namespace StackOverflowAnswers;
public class CustomValidation : ComponentBase, IDisposable
{
private IDisposable? _subscriptions;
private EditContext? _originalEditContext;
[CascadingParameter] EditContext? CurrentEditContext { get; set; }
[Parameter] public bool DoEditValidation { get; set; } = false;
/// <inheritdoc />
protected override void OnInitialized()
{
if (CurrentEditContext == null)
{
throw new InvalidOperationException($"{nameof(DataAnnotationsValidator)} requires a cascading " +
$"parameter of type {nameof(EditContext)}. For example, you can use {nameof(DataAnnotationsValidator)} " +
$"inside an EditForm.");
}
_subscriptions = CurrentEditContext.EnableCustomValidation(DoEditValidation, true);
_originalEditContext = CurrentEditContext;
}
/// <inheritdoc />
protected override void OnParametersSet()
{
if (CurrentEditContext != _originalEditContext)
{
// While we could support this, there's no known use case presently. Since InputBase doesn't support it,
// it's more understandable to have the same restriction.
throw new InvalidOperationException($"{GetType()} does not support changing the " +
$"{nameof(EditContext)} dynamically.");
}
}
/// <inheritdoc/>
protected virtual void Dispose(bool disposing)
{
}
void IDisposable.Dispose()
{
_subscriptions?.Dispose();
_subscriptions = null;
Dispose(disposing: true);
}
}
You can use it like this:
<EditForm EditContext=this.editContext OnValidSubmit=OnValidSubmit>
<CustomValidation DoEditValidation=false/>
#*<DataAnnotationsValidator/>*#
<div class="row">
<div class="col-2">
Date:
</div>
<div class="col-10">
<InputDate #bind-Value=this.Record.Date></InputDate>
</div>
</div>
.......

How do you hand over files to the user?

in a #WASM / #UNO-platform project, how do you hand over files to the user?
In my case I’m generation locally a PDF and had to download it or display it in the browser.
Any clue?
Regards,
Michael
There's no API to do that directly, yet. But you can create a data: url on an anchor (a) HTML element.
For this you'll need to create some JavaScript. Here's how you can do it:
IMPORTANT: following code will only work with very recent version of Uno.UI. Version starting with v3.0.0-dev.949+
Create a ContentControl for the <a> tag
[HtmlElement("a")]
public partial class WasmDownload : ContentControl
{
public static readonly DependencyProperty MimeTypeProperty = DependencyProperty.Register(
"MimeType", typeof(string), typeof(WasmDownload), new PropertyMetadata("application/octet-stream", OnChanged));
public string MimeType
{
get => (string) GetValue(MimeTypeProperty);
set => SetValue(MimeTypeProperty, value);
}
public static readonly DependencyProperty FileNameProperty = DependencyProperty.Register(
"FileName", typeof(string), typeof(WasmDownload), new PropertyMetadata("filename.bin", OnChanged));
public string FileName
{
get => (string) GetValue(FileNameProperty);
set => SetValue(FileNameProperty, value);
}
private Memory<byte> _content;
public void SetContent(Memory<byte> content)
{
_content = content;
Update();
}
private static void OnChanged(DependencyObject dependencyobject, DependencyPropertyChangedEventArgs args)
{
if (dependencyobject is WasmDownload wd)
{
wd.Update();
}
}
private void Update()
{
if (_content.Length == 0)
{
this.ClearHtmlAttribute("href");
}
else
{
var base64 = Convert.ToBase64String(_content.ToArray());
var dataUrl = $"data:{MimeType};base64,{base64}";
this.SetHtmlAttribute("href", dataUrl);
this.SetHtmlAttribute("download", FileName);
}
}
}
Use it in Your XAML Page
<myControls:WasmDownload FileName="test.txt" x:Name="download">
Click here to download
</myControls:WasmDownload>
Note you can put anything in the content of your control, as any other XAML ContentControl.
Set the File Content in Code Behind
Loaded += (sender, e) =>
{
download.MimeType = "text/plain";
var bytes = Encoding.UTF8.GetBytes("this is the content");
download.SetContent(bytes);
};
Result
Direct support by Uno
There is a PR #3380 to add this feature to Uno natively for all platforms. You can also wait for it instead of doing custom way.
The PR for FileSavePicker has been merged and the feature is now available in package Uno.UI since version 3.0.0-dev.1353.

Create the own VirtualPathProvider in MVC4?

I'm suffering trying to get some views from a library to the main project. I was starting to read about creating your own VirtualPathProvider implementation here: Using VirtualPathProvider to load ASP.NET MVC views from DLLs
I had to set my view = EmbbebedResource to get the resource from the library. But now is throwing another error.
In the header of my partial view I had the following:
#model Contoso.ExercisesLibrary.AbsoluteArithmetic.Problem1
And the error says: External component has thrown an exception. c:\Users\Oscar\AppData\Local\Temp\Temporary ASP.NET Files\root\4f78c765\7f9a47c6\App_Web_contoso.exerciseslibrary.absolutearithmetic.view1.cshtml.38e14c22.y-yjyt6g.0.cs(46): error CS0103: The name 'model' does not exist in the current context
I don't know why the compiler tells that cannot recognized my model. When I'm in design mode, I can see the compiler that the check is all right.
Check the image
What am I doing wrong o what am I missing?
Thanks in advance.
Try adding an #inherits directive to the top of your razor view:
#inherits System.Web.Mvc.WebViewPage
#model Contoso.ExercisesLibrary.AbsoluteArithmetic.Problem1
The reason you need this is because your view comes from an embedded resource and not from the standard ~/Views location. And as you know inside ~/Views there's a file called web.config. And inside this file there's a pageBaseType="System.Web.Mvc.WebViewPage" directive indicating that all Razor files inside ~/Views should inherit from this base type. But since your view is now coming from an unknown location you have nowhere specified that it should be a System.Web.Mvc.WebViewPage. And all the MVC specific stuff such as models, HTML helpers, ... are defined in this base class.+
I faced this issue "The name 'model' does not exist in the current context". What I did was added same "areas" folder structure (from my embedded mvc project) to my main MVC project (Areas/AnualReports/Views/) and copied web.config (default web.config from views folder, not the one from root) to Views folder which solved the issue. I am not sure this will work in your case.
Update:
Adding web.config (from views folder) to root "areas" folder in main MVC project also works.
I have the same problem as you so after all searches I got working solution
Create your own WebViewPage based abstract class (generic for model and non generic)
public abstract class MyOwnViewPage<TModel> : WebViewPage<TModel> { }
public abstract class MyOwnViewPage : WebViewPage { }
Next create VirtualFile based class or embedded view's
class AssemblyResourceFile : VirtualFile
{
private readonly IDictionary<string, Assembly> _nameAssemblyCache;
private readonly string _assemblyPath;
private readonly string _webViewPageClassName;
public string LayoutPath { get; set; }
public string ViewStartPath { get; set; }
public AssemblyResourceFile(IDictionary<string, Assembly> nameAssemblyCache, string virtualPath) :
base(virtualPath)
{
_nameAssemblyCache = nameAssemblyCache;
_assemblyPath = VirtualPathUtility.ToAppRelative(virtualPath);
LayoutPath = "~/Views/Shared/_Layout.cshtml";
ViewStartPath = "~/Views/_ViewStart.cshtml";
_webViewPageClassName = typeofMyOwnViewPage).ToString();
}
// Please change Open method for your scenario
public override Stream Open()
{
string[] parts = _assemblyPath.Split(new[] { '/' }, 4);
string assemblyName = parts[2];
string resourceName = parts[3].Replace('/', '.');
Assembly assembly;
lock (_nameAssemblyCache)
{
if (!_nameAssemblyCache.TryGetValue(assemblyName, out assembly))
{
var assemblyPath = Path.Combine(HttpRuntime.BinDirectory, assemblyName);
assembly = Assembly.LoadFrom(assemblyPath);
_nameAssemblyCache[assemblyName] = assembly;
}
}
Stream resourceStream = null;
if (assembly != null)
{
resourceStream = assembly.GetManifestResourceStream(resourceName);
if (resourceName.EndsWith(".cshtml"))
{
//the trick is here. We must correct our embedded view
resourceStream = CorrectView(resourceName, resourceStream);
}
}
return resourceStream;
}
public Stream CorrectView(string virtualPath, Stream stream)
{
var reader = new StreamReader(stream, Encoding.UTF8);
var view = reader.ReadToEnd();
stream.Close();
var ourStream = new MemoryStream();
var writer = new StreamWriter(ourStream, Encoding.UTF8);
var modelString = "";
var modelPos = view.IndexOf("#model");
if (modelPos != -1)
{
writer.Write(view.Substring(0, modelPos));
var modelEndPos = view.IndexOfAny(new[] { '\r', '\n' }, modelPos);
modelString = view.Substring(modelPos, modelEndPos - modelPos);
view = view.Remove(0, modelEndPos);
}
writer.WriteLine("#using System.Web.Mvc");
writer.WriteLine("#using System.Web.Mvc.Ajax");
writer.WriteLine("#using System.Web.Mvc.Html");
writer.WriteLine("#using System.Web.Routing");
var basePrefix = "#inherits " + _webViewPageClassName;
if (virtualPath.ToLower().Contains("_viewstart"))
{
writer.WriteLine("#inherits System.Web.WebPages.StartPage");
}
else if (modelString == "#model object")
{
writer.WriteLine(basePrefix + "<dynamic>");
}
else if (!string.IsNullOrEmpty(modelString))
{
writer.WriteLine(basePrefix + "<" + modelString.Substring(7) + ">");
}
else
{
writer.WriteLine(basePrefix);
}
writer.Write(view);
writer.Flush();
ourStream.Position = 0;
return ourStream;
}
}
Next create VirtualPathProvider based class (modify it for your purposes)
public class AssemblyResPathProvider : VirtualPathProvider
{
private readonly Dictionary<string, Assembly> _nameAssemblyCache;
private string _layoutPath;
private string _viewstartPath;
public AssemblyResPathProvider(string layout, string viewstart)
{
_layoutPath = layout;
_viewstartPath = viewstart;
_nameAssemblyCache = new Dictionary<string, Assembly>(StringComparer.InvariantCultureIgnoreCase);
}
private bool IsAppResourcePath(string virtualPath)
{
string checkPath = VirtualPathUtility.ToAppRelative(virtualPath);
bool bres1 = checkPath.StartsWith("~/App_Resource/",
StringComparison.InvariantCultureIgnoreCase);
bool bres2 = checkPath.StartsWith("/App_Resource/",
StringComparison.InvariantCultureIgnoreCase);
//todo: fix this
if (checkPath.EndsWith("_ViewStart.cshtml"))
{
return false;
}
if (checkPath.EndsWith("_ViewStart.vbhtml"))
{
return false;
}
return ((bres1 || bres2));
}
public override bool FileExists(string virtualPath)
{
return (IsAppResourcePath(virtualPath) ||
base.FileExists(virtualPath));
}
public override VirtualFile GetFile(string virtualPath)
{
if (IsAppResourcePath(virtualPath))
{
// creating AssemblyResourceFile instance
return new AssemblyResourceFile(_nameAssemblyCache, virtualPath,_layoutPath,virtualPath);
}
return base.GetFile(virtualPath);
}
public override CacheDependency GetCacheDependency(
string virtualPath,
IEnumerable virtualPathDependencies,
DateTime utcStart)
{
if (IsAppResourcePath(virtualPath))
{
return null;
}
return base.GetCacheDependency(virtualPath,
virtualPathDependencies, utcStart);
}
}
At last register your AssemblyResPathProvider in global.asax
string _layoutPath = "~/Views/Shared/_Layout.cshtml";
string _viewstarPath = "~/Views/_ViewStart.cshtml";
HostingEnvironment.RegisterVirtualPathProvider(new AssemblyResPathProvider(_layoutPath,_viewstarPath));
This is not ideal solution but its working for me good. Cheers!
In my case, the solution was to make the virtual Path start with "~Views/" - just like any normal view.
Not working: ~/VIRTUAL/Home/Index.cshtml
Working: ~/Views/VIRTUAL/Home/Index.cshtml
I think, this has to do with the web.config lying around in ~/Views and defining a lot of stuff for the views. Maybe anybody can give more information.
Hope that helps anyway.

Binding with Map API Extensions for Windows Phone 8

I'm trying to use databinding with the map api extension of windows phone toolkit. I'm doing :
<maps:Map x:Name="Map" Center="47.6, -122.3" ZoomLevel="12">
<maptk:MapExtensions.Children>
<maptk:MapItemsControl ItemsSource="{Binding PositionList}">
<maptk:MapItemsControl.ItemTemplate>
<DataTemplate>
<maptk:Pushpin GeoCoordinate="{Binding}" />
</DataTemplate>
</maptk:MapItemsControl.ItemTemplate>
</maptk:MapItemsControl>
</maptk:MapExtensions.Children>
</maps:Map>
with my code behind :
public partial class MainPage : PhoneApplicationPage, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string name)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
private bool NotifyPropertyChanged<T>(ref T variable, T valeur, [CallerMemberName] string name= null)
{
if (object.Equals(variable, valeur)) return false;
variable = valeur;
NotifyPropertyChanged(name);
return true;
}
private IEnumerable<GeoCoordinate> positionList;
public IEnumerable<GeoCoordinate> PositionList
{
get { return positionList; }
set { NotifyPropertyChanged(ref positionList, value); }
}
public MainPage()
{
InitializeComponent();
PositionList = new List<GeoCoordinate>
{
new GeoCoordinate(47.6050338745117, -122.334243774414),
new GeoCoordinate(47.6045697927475, -122.329885661602),
new GeoCoordinate(47.605712890625, -122.330268859863),
new GeoCoordinate(47.6015319824219, -122.335113525391),
new GeoCoordinate(47.6056594848633, -122.334243774414)
};
DataContext = this;
}
}
But I can't see any pushpin on the map :(
What am I doing wrong ?
Note that If I use this in the code-behind file, it's working
MapExtensions.GetChildren(Map).OfType<MapItemsControl>().First().ItemsSource = PositionList;
Thanks in advance for your help,
Best regards
MapItemsControl derives from DependencyObject, not FrameworkElement so the DataContext does not propagate. Long story long... you can't data bind MapItemsControl from XAML unless you have some way to set the Source property of the Binding.
If the FindAncestor mode of RelativeSource worked on the phone, it might be possible to work around this but it apparently does not. This leaves us with either creating the binding in code or (more realistically) setting the ItemsSource in code.

Resources