public ObservableCollection<WordList> MyWordList { get; set; }
public DictionaryPage()
{
InitializeComponent();
BindingContext = new DictionaryPageViewModel();
MyWordList = new ObservableCollection<WordList>
{
new WordList { Color = "Red", Letter = "A", Word = "Abdomen", Meaning = "Mean : " + "Mean",Detail= "Mean", Voice = "myVoice.mp3" }
};
}
private async void PronunciationButton_Clicked(object sender, System.EventArgs e)
{
await CrossMediaManager.Current.PlayFromAssembly("HERE HERE HERE");
}
I have an observable collection like this and I want to put the voice value in it to the button event below. Thanks for your help
MyWordList is a class level variable. Just reference it in your Clicked handler
private async void PronunciationButton_Clicked(object sender, System.EventArgs e)
{
For each (var word in MyWordList) {
await CrossMediaManager.Current.PlayFromAssembly(word.Voice);
}
}
I'm clicking on a product item in listview in product page viewmodel to show a popup(using rg.plugin popup) for selecting one of the product variants.After selecting variant,i am sending the selected variant to product page using messagingcenter from variant popup page viewmodel,subscribed in product page viewmodel constructor. working fine there.when i navigate to the previous page and then came back to this product page for adding one or more variant to the
same previously selected product,Messagingcenter subscribe called twice and product value increased twice.Tried to subscribe in the product page onappearing and unsubscribe in disappearing method.still calling two times? How to solve this issue?
calling popup:
var result = await dataService.Get_product_variant(store_id, product_id);
if (result.status == "success")
{
ind_vis = false;
OnPropertyChanged("ind_vis");
App.Current.Properties["product_variant_result"] = result;
App.Current.Properties["cartitems"] = purchaselist;
App.Current.Properties["selected_product"] = product_List2 ;
await PopupNavigation.Instance.PushAsync(new Popup_variant());
}
popup viewmodel: sending message
public Popup_variant_vm()
{
Radio_btn = new Command<Product_variant_list2>(Radio_stk_tapped);
product_variant_list = new List<Product_variant_list2>();
purchaselist = new ObservableCollection<Product_list2>();
show_variants();
}
internal void Confirm_variant()
{
if(App.Current.Properties.ContainsKey("selected_variant"))
{
MessagingCenter.Send<Popup_variant_vm, object>(this, "selected_variant", App.Current.Properties["selected_variant"]); //Message send from popup to product page
}
else
{
DependencyService.Get<IToast>().LongAlert("Please select any size");
}
}
product page viewmodel: subscribed here..called twice when navigating from previous page to this
public Store_page()
{
InitializeComponent();
}
protected override void OnAppearing()
{
base.OnAppearing();
var vm = new store_page_vm();
vm.Navigation = Navigation;
BindingContext = vm;
MessagingCenter.Unsubscribe<Popup_variant_vm, object>(this, "selected_variant");
MessagingCenter.Subscribe<Popup_variant_vm, object>(this, "selected_variant",async (sender, selected_variant) =>
{
var vm1 = BindingContext as store_page_vm;
vm1?.Addcart2(selected_variant);// called twice
});
}
unsubscribed in product cs page
protected override void OnDisappearing()
{
var vm = BindingContext as store_page_vm;
vm?.Save_cart();
MessagingCenter.Unsubscribe<Popup_variant_vm>(this, "selected_variant");
}
Your unsubscription should look something like below and it should work :
MessagingCenter.Unsubscribe<Popup_variant_vm, object>(this, "selected_variant");
https://stackoverflow.com/a/44753021/10937160
try this, and make sure you do not call Subscribe more than once.
My solution:
put unsubscribe sentence into subscribe body !!
MessagingCenter.Subscribe<object, string>(this, "IdSearch", (sender, arg) =>
{
listView.ItemsSource = arg;
MessagingCenter.Unsubscribe<object, string>(this, "IdSearch");
}, BindingContext);
I have created static counter variable in my app the in subscriber I have done this:
public static class Constants
{
public static int msgCenterSubscribeCounter { get; set; } = 0;
}
MessagingCenter.Subscribe<object, string>(this, "hello", (sender, arg) =>
{
Constants.msgCenterSubscribeCounter++;
if (arg.Equals("hello") && Constants.msgCenterSubscribeCounter == 1)
{
// handle your logic here
}
});
Reset counter in OnDisappearing() method from where you have called Send.
Changing Messagingcenter in to single subscription.
public class Messagingcenter_singleton
{
private static Messagingcenter_singleton _instance;
private bool isActivated = false;
private Action<string> callBackFun = null;
public static Messagingcenter_singleton Instance()
{
if (_instance == null)
{
_instance = new Messagingcenter_singleton();
}
return _instance;
}
public void setCallBack(Action<string> eventCallBack)
{
callBackFun = eventCallBack;
}
public void startSubscribe()
{
if (!isActivated)
{
isActivated = true;
MessagingCenter.Subscribe<string, string>(this, "Name", eventCallBack);
}
}
private void eventCallBack(string arg1, string arg2)
{
if (callBackFun != null)
{
InvokeMethod(new Action<string>(callBackFun), arg2);
}
}
public static object InvokeMethod(Delegate method, params object[] args)
{
return method.DynamicInvoke(args);
}
}
Use Below Code in you view model class
public void initSubscribe()
{
Messagingcenter_singleton.Instance().startSubscribe();
Messagingcenter_singleton.Instance().setCallBack(eventCallBack)
}
public void eventCallBack(string arg2)
{
// write your code here
}
I trying to scan QR-codes in my app. It works sometimes and sometimes not. The feeling is that it doesn't work when something is loading on another thread. For example, if I press the scan button when nearby stores still are loading. Often it works if I wait.
The video capture is working because I will see the preview.
Here is my code:
private bool resultFound;
public ScannerView()
{
InitializeComponent();
On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUseSafeArea(false);
view = new UIView();
ScannerArea.Children.Add(view);
}
void HandleAVRequestAccessStatus(bool accessGranted)
{
if (!accessGranted)
{
MainThread.BeginInvokeOnMainThread(async () =>
{
await NavigationHelper.Current.CloseModalAsync();
});
}
metadataOutput = new AVCaptureMetadataOutput();
var metadataDelegate = new MetadataOutputDelegate();
metadataOutput.SetDelegate(metadataDelegate, DispatchQueue.MainQueue);
session = new AVCaptureSession();
camera = AVCaptureDevice.DevicesWithMediaType(AVMediaType.Video).First();
input = AVCaptureDeviceInput.FromDevice(camera);
session.AddInput(input);
session.AddOutput(metadataOutput);
metadataOutput.MetadataObjectTypes = AVMetadataObjectType.QRCode | AVMetadataObjectType.EAN8Code | AVMetadataObjectType.EAN13Code;
metadataDelegate.MetadataFound += MetadataDelegate_MetadataFound;
camera.LockForConfiguration(out var error);
camera.VideoZoomFactor = 2;
camera.UnlockForConfiguration();
layer = new AVCaptureVideoPreviewLayer(session);
layer.VideoGravity = AVLayerVideoGravity.ResizeAspectFill;
layer.MasksToBounds = true;
layer.Frame = UIApplication.SharedApplication.KeyWindow.RootViewController.View.Bounds;
view.Layer.AddSublayer(layer);
session.StartRunning();
}
protected override void OnAppearing()
{
base.OnAppearing();
var status = AVCaptureDevice.GetAuthorizationStatus(AVMediaType.Video);
if (status != AVAuthorizationStatus.Authorized)
{
MainThread.BeginInvokeOnMainThread(() =>
{
AVCaptureDevice.RequestAccessForMediaType(AVMediaType.Video, HandleAVRequestAccessStatus);
});
return;
}
HandleAVRequestAccessStatus(true);
}
protected override void OnDisappearing()
{
base.OnDisappearing();
session.StopRunning();
session.RemoveInput(input);
session = null;
}
void Handle_Clicked(object sender, System.EventArgs e)
{
NavigationHelper.Current.CloseModalAsync();
}
void MetadataDelegate_MetadataFound(object sender, AVMetadataMachineReadableCodeObject e)
{
if (resultFound)
{
return;
}
resultFound = true;
var text = e.StringValue;
Device.BeginInvokeOnMainThread(async () =>
{
Vibration.Vibrate(100);
await NavigationHelper.Current.CloseModalAsync();
TinyPubSub.Publish(NavigationParameter.ToString(), text);
});
}
}
public class MetadataOutputDelegate : AVCaptureMetadataOutputObjectsDelegate
{
public override void DidOutputMetadataObjects(AVCaptureMetadataOutput captureOutput, AVMetadataObject[] metadataObjects, AVCaptureConnection connection)
{
foreach (var m in metadataObjects)
{
if (m is AVMetadataMachineReadableCodeObject)
{
MetadataFound(this, m as AVMetadataMachineReadableCodeObject);
}
}
}
public event EventHandler<AVMetadataMachineReadableCodeObject> MetadataFound = delegate { };
}
I'm using Xamarin forms and writing a dependency service for the following objectives :
Open iOS files app. (UIDocumentPickerViewController )
Select any kind of a document.
Copy that document into my application Documents directory. (For app access)
Show that document into my application by storing its path into my SQLite DB.
What I am trying to do here is call the Files app from my application on an Entry click and the click event seems to be working well my dependency service calls perfectly but now when I try to use the UIDocumentPickerViewController I am unable to get View controller context in my dependency service to call the PresentViewController method. Now I know about the xamarin forms context but I don't know if it will work here and I don't even know if it would be a smart idea to use it as it has already been marked as obsolete and since I am not from the iOS background, I don't know what would be the right solution for it.
My code is as follows :
public class DocumentPickerRenderer : IDocumentPicker
{
public object PickFile()
{
var docPicker = new UIDocumentPickerViewController(new string[] { UTType.Data, UTType.Content }, UIDocumentPickerMode.Import);
docPicker.WasCancelled += (sender, wasCancelledArgs) =>
{
};
docPicker.DidPickDocumentAtUrls += (object sender, UIDocumentPickedAtUrlsEventArgs e) =>
{
Console.WriteLine("url = {0}", e.Urls[0].AbsoluteString);
//bool success = await MoveFileToApp(didPickDocArgs.Url);
var success = true;
string filename = e.Urls[0].LastPathComponent;
string msg = success ? string.Format("Successfully imported file '{0}'", filename) : string.Format("Failed to import file '{0}'", filename);
var alertController = UIAlertController.Create("import", msg, UIAlertControllerStyle.Alert);
var okButton = UIAlertAction.Create("OK", UIAlertActionStyle.Default, (obj) =>
{
alertController.DismissViewController(true, null);
});
alertController.AddAction(okButton);
PresentViewController(alertController, true, null);
};
PresentViewController(docPicker, true, null);
}
}
My questions:
Is my methodology correct for picking files?
what will be the object that I will be getting as a callback from a file selection and how will I get the callback?
Is there any other way or something available for xamarin forms, some guide or something that allows me to pick documents from my native file systems and gives a brief on how to handle it in both ios and android?
Hello Guys, You can use following code for picking any type of documents to mention in code using iOS Devices-
use follwing interface:
public interface IMedia
{
Task<string> OpenDocument();
}
public Task<string> OpenDocument()
{
var task = new TaskCompletionSource<string>();
try
{
OpenDoc(GetController(), (obj) =>
{
if (obj == null)
{
task.SetResult(null);
return;
}
var aa = obj.AbsoluteUrl;
task.SetResult(aa.Path);
});
}
catch (Exception ex)
{
task.SetException(ex);
}
return task.Task;
}
static Action<NSUrl> _callbackDoc;
public static void OpenDoc(UIViewController parent, Action<NSUrl> callback)
{
_callbackDoc = callback;
var version = UIDevice.CurrentDevice.SystemVersion;
int verNum = 0;
Int32.TryParse(version.Substring(0, 2), out verNum);
var allowedUTIs = new string[]
{
UTType.UTF8PlainText,
UTType.PlainText,
UTType.RTF,
UTType.PNG,
UTType.Text,
UTType.PDF,
UTType.Image,
UTType.Spreadsheet,
"com.microsoft.word.doc",
"org.openxmlformats.wordprocessingml.document",
"com.microsoft.powerpoint.ppt",
"org.openxmlformats.spreadsheetml.sheet",
"org.openxmlformats.presentationml.presentation",
"com.microsoft.excel.xls",
};
// Display the picker
var pickerMenu = new UIDocumentMenuViewController(allowedUTIs, UIDocumentPickerMode.Import);
pickerMenu.DidPickDocumentPicker += (sender, args) =>
{
if (verNum < 11)
{
args.DocumentPicker.DidPickDocument += (sndr, pArgs) =>
{
UIApplication.SharedApplication.OpenUrl(pArgs.Url);
pArgs.Url.StopAccessingSecurityScopedResource();
var cb = _callbackDoc;
_callbackDoc = null;
pickerMenu.DismissModalViewController(true);
cb(pArgs.Url.AbsoluteUrl);
};
}
else
{
args.DocumentPicker.DidPickDocumentAtUrls += (sndr, pArgs) =>
{
UIApplication.SharedApplication.OpenUrl(pArgs.Urls[0]);
pArgs.Urls[0].StopAccessingSecurityScopedResource();
var cb = _callbackDoc;
_callbackDoc = null;
pickerMenu.DismissModalViewController(true);
cb(pArgs.Urls[0].AbsoluteUrl);
};
}
// Display the document picker
parent.PresentViewController(args.DocumentPicker, true, null);
};
pickerMenu.ModalPresentationStyle = UIModalPresentationStyle.Popover;
parent.PresentViewController(pickerMenu, true, null);
UIPopoverPresentationController presentationPopover = pickerMenu.PopoverPresentationController;
if (presentationPopover != null)
{
presentationPopover.SourceView = parent.View;
presentationPopover.PermittedArrowDirections = UIPopoverArrowDirection.Down;
}
}
Now you need to call using following code:
var filePath = await DependencyService.Get<IMedia>().OpenDocument();
For pick document in Android, you can use following code
public class IntentHelper
{
public const int DocPicker = 101;
static Action<string> _callback;
public static async void ActivityResult(int requestCode, Result resultCode, Intent data)
{ if (requestCode == RequestCodes.DocPicker)
{
if (data.Data == null)
{
_callback(null);
}
else
{
var destFilePath = FilePath.GetPath(CurrentActivity, data.Data);
_callback(destFilePath);
}
}
}
public static Activity CurrentActivity
{
get
{
return (Xamarin.Forms.Forms.Context as MainActivity);
}
}
public static void OpenDocPicker(Action<string> callback)
{
_callback = callback;
var intent = new Intent(Intent.ActionOpenDocument);
intent.AddCategory(Intent.CategoryOpenable);
intent.SetType("*/*");
CurrentActivity.StartActivityForResult(intent, RequestCodes.DocPicker);
}
}
For pick document in Android, you can use following code:
public class IntentHelper
{
public const int DocPicker = 101;
static Action<string> _callback;
public static async void ActivityResult(int requestCode, Result resultCode, Intent data)
{
if (requestCode == RequestCodes.DocPicker)
{
if (data.Data == null)
{
_callback(null);
}
else
{
var destFilePath = FilePath.GetPath(CurrentActivity, data.Data);
_callback(destFilePath);
}
}
}
public static Activity CurrentActivity
{
get
{
return (Xamarin.Forms.Forms.Context as MainActivity);
}
}
public static void OpenDocPicker(Action<string> callback)
{
_callback = callback;
var intent = new Intent(Intent.ActionOpenDocument);
intent.AddCategory(Intent.CategoryOpenable);
intent.SetType("*/*");
CurrentActivity.StartActivityForResult(intent, RequestCodes.DocPicker);
}
}
Use below code to access the helper class: public class Media:
IMedia {
public Task<string> OpenDocument() {
var task = new TaskCompletionSource<string>();
try {
IntentHelper.OpenDocPicker((path) => { task.SetResult(path); });
} catch (Exception ex) {
task.SetResult(null);
}
return task.Task;
}
}
Since I was looking for UIDocumentPickerViewController and not UIDocumentMenuViewController the other answer was not what I was looking for :
So this is how I ended up doing it:
Calling the document picker:
var docPicker = new UIDocumentPickerViewController(new string[]
{ UTType.Data, UTType.Content }, UIDocumentPickerMode.Import);
docPicker.WasCancelled += DocPicker_WasCancelled;
docPicker.DidPickDocumentAtUrls += DocPicker_DidPickDocumentAtUrls;
docPicker.DidPickDocument += DocPicker_DidPickDocument;
var _currentViewController = GetCurrentUIController();
if (_currentViewController != null)
_currentViewController.PresentViewController(docPicker, true, null);
Where GetCurrentUIController is the function to get the current UI controller something like this :
public UIViewController GetCurrentUIController()
{
UIViewController viewController;
var window = UIApplication.SharedApplication.KeyWindow;
if (window == null)
{
return null;
}
if (window.RootViewController.PresentedViewController == null)
{
window = UIApplication.SharedApplication.Windows
.First(i => i.RootViewController != null &&
i.RootViewController.GetType().FullName
.Contains(typeof(Xamarin.Forms.Platform.iOS.Platform).FullName));
}
viewController = window.RootViewController;
while (viewController.PresentedViewController != null)
{
viewController = viewController.PresentedViewController;
}
return viewController;
}
For below iOS 11 i added the DidPickDocument event:
private void DocPicker_DidPickDocument(object sender, UIDocumentPickedEventArgs e)
{
try
{
NSUrl filePath = e.Url.AbsoluteUrl;
//This is the url for your document and you can use it as you please.
}
catch (Exception ex)
{
}
}
For above iOS 11 you use the DidPickDocumentUrls since multipick is supported there :
private void DocPicker_DidPickDocumentAtUrls(object sender, UIDocumentPickedAtUrlsEventArgs e)
{
try
{
List<NSUrl> filePath = e.Urls.ToList().Select(y => y.AbsoluteUrl).ToList();
//returns the list of images selected
}
catch (Exception ex)
{
AppLogger.LogException(ex);
}
}
If you are searching for a full polylines, pins, tiles, UIOptions (and 3D effects soon) renderings/implementations, you should take a loot at the public github I made at XamarinByEmixam23/..../Map.
I search a lot but I still have the same problem:
How can I update, refresh or reload the Xamarin.Forms.Maps?
In the class definition (class CustomMap : Map), there is no method to update the maps.. Maybe a MVVM logic can solves the problem, but I can't find it on the Web..
I followed this tutorial for the maps : Working with maps
To customise it, I followed this tutorial : Highlight a Route on a Map
So, after these tutorials (I made the same things, no changes), I tried with 2 RouteCoordinates which gave me a straight line... I then made an algorithm which works perfectly.
DirectionMap
public class DirectionMap
{
public Distance distance { get; set; }
public Duration duration { get; set; }
public Address address_start { get; set; }
public Address address_end { get; set; }
public List<Step> steps { get; set; }
public class Distance
{
public string text { get; set; }
public int value { get; set; }
}
public class Duration
{
public string text { get; set; }
public int value { get; set; }
}
public class Address
{
public string text { get; set; }
public Position position { get; set; }
}
public class Step
{
public Position start { get; set; }
public Position end { get; set; }
}
}
ResponseHttpParser
public static void parseDirectionGoogleMapsResponse(HttpStatusCode httpStatusCode, JObject json, Action<DirectionMap, string> callback)
{
switch (httpStatusCode)
{
case HttpStatusCode.OK:
DirectionMap directionMap = null;
string strException = null;
try
{
directionMap = new DirectionMap()
{
distance = new DirectionMap.Distance()
{
text = (json["routes"][0]["legs"][0]["distance"]["text"]).ToString(),
value = Int32.Parse((json["routes"][0]["legs"][0]["distance"]["value"]).ToString())
},
duration = new DirectionMap.Duration()
{
text = (json["routes"][0]["legs"][0]["duration"]["text"]).ToString(),
value = Int32.Parse((json["routes"][0]["legs"][0]["duration"]["value"]).ToString())
},
address_start = new DirectionMap.Address()
{
text = (json["routes"][0]["legs"][0]["start_address"]).ToString(),
position = new Position(Double.Parse((json["routes"][0]["legs"][0]["start_location"]["lat"]).ToString()), Double.Parse((json["routes"][0]["legs"][0]["start_location"]["lng"]).ToString()))
},
address_end = new DirectionMap.Address()
{
text = (json["routes"][0]["legs"][0]["end_address"]).ToString(),
position = new Position(Double.Parse((json["routes"][0]["legs"][0]["end_location"]["lat"]).ToString()), Double.Parse((json["routes"][0]["legs"][0]["end_location"]["lng"]).ToString()))
}
};
bool finished = false;
directionMap.steps = new List<Step>();
int index = 0;
while (!finished)
{
try
{
Step step = new Step()
{
start = new Position(Double.Parse((json["routes"][0]["legs"][0]["steps"][index]["start_location"]["lat"]).ToString()), Double.Parse((json["routes"][0]["legs"][0]["steps"][index]["start_location"]["lng"]).ToString())),
end = new Position(Double.Parse((json["routes"][0]["legs"][0]["steps"][index]["end_location"]["lat"]).ToString()), Double.Parse((json["routes"][0]["legs"][0]["steps"][index]["end_location"]["lng"]).ToString()))
};
directionMap.steps.Add(step);
index++;
}
catch (Exception e)
{
finished = true;
}
}
}
catch (Exception e)
{
directionMap = null;
strException = e.ToString();
}
finally
{
callback(directionMap, strException);
}
break;
default:
switch (httpStatusCode)
{
}
callback(null, json.ToString());
break;
}
}
I just get the distance and duration for some private calculs and get each step that I put into a List<>;
When everything is finished, I use my callback which bring us back to the controller (MapPage.xaml.cs the XAML Form Page (Xamarin Portable))
Now, everything becomes weird. It's like the map doesn't get that changes are made
public partial class MapPage : ContentPage
{
public MapPage()
{
InitializeComponent();
setupMap();
setupMapCustom();
}
public void setupMapCustom()
{
customMap.RouteCoordinates.Add(new Position(37.785559, -122.396728));
customMap.RouteCoordinates.Add(new Position(37.780624, -122.390541));
customMap.RouteCoordinates.Add(new Position(37.777113, -122.394983));
customMap.RouteCoordinates.Add(new Position(37.776831, -122.394627));
customMap.MoveToRegion(MapSpan.FromCenterAndRadius(new Position(37.79752, -122.40183), Xamarin.Forms.Maps.Distance.FromMiles(1.0)));
}
public async void setupMap()
{
customMap.MapType = MapType.Satellite;
string origin = "72100 Le Mans";
string destination = "75000 Paris";
HttpRequest.getDirections(origin, destination, callbackDirections);
customMap.RouteCoordinates.Add(await MapUtilities.GetMapPointOfStreetAddress(origin));
Position position = await MapUtilities.GetMapPointOfStreetAddress(destination);
//customMap.RouteCoordinates.Add(position);
var pin = new Pin
{
Type = PinType.Place,
Position = position,
Label = "Destination !!",
};
customMap.Pins.Add(pin);
}
private async void callbackDirections(Object obj, string str)
{
if (obj != null)
{
DirectionMap directionMap = obj as DirectionMap;
foreach (Step step in directionMap.steps)
{
customMap.RouteCoordinates.Add(step.start);
System.Diagnostics.Debug.WriteLine("add step");
}
customMap.RouteCoordinates.Add(directionMap.address_end.position);
System.Diagnostics.Debug.WriteLine("add last step");
}
else
{
System.Diagnostics.Debug.WriteLine(str);
}
}
}
I run my app, everything works until it's something fast, because of the time spent by my algorithm etc, the callback is coming too late and then I need to refresh, reload or update my map... Anyway, I need to update my map in the future, so... If anyone can help, this one is welcome !
EDIT 1
I took a look at your answer ( thank a lot ! ;) ) but it doesn't works :/
I updated CustomMap as you did
public class CustomMap : Map
{
public static readonly BindableProperty RouteCoordinatesProperty =
BindableProperty.Create<CustomMap, List<Position>>(p => p.RouteCoordinates, new List<Position>());
public List<Position> RouteCoordinates
{
get { return (List<Position>)GetValue(RouteCoordinatesProperty); }
set { SetValue(RouteCoordinatesProperty, value); }
}
public CustomMap()
{
RouteCoordinates = new List<Position>();
}
}
Same for CustomMapRenderer (Droid)
public class CustomMapRenderer : MapRenderer, IOnMapReadyCallback
{
GoogleMap map;
Polyline polyline;
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<Xamarin.Forms.View> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
// Unsubscribe
}
if (e.NewElement != null)
{
((MapView)Control).GetMapAsync(this);
}
}
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (this.Element == null || this.Control == null)
return;
if (e.PropertyName == CustomMap.RouteCoordinatesProperty.PropertyName)
{
UpdatePolyLine();
}
}
private void UpdatePolyLine()
{
if (polyline != null)
{
polyline.Remove();
polyline.Dispose();
}
var polylineOptions = new PolylineOptions();
polylineOptions.InvokeColor(0x66FF0000);
foreach (var position in ((CustomMap)this.Element).RouteCoordinates)
{
polylineOptions.Add(new LatLng(position.Latitude, position.Longitude));
}
polyline = map.AddPolyline(polylineOptions);
}
public void OnMapReady(GoogleMap googleMap)
{
map = googleMap;
UpdatePolyLine();
}
}
So, for the last change, in my MapPage.xaml.cs I made changes in the callbackDirections as you explained (I hope I did good)
private async void callbackDirections(Object obj, string str)
{
if (obj != null)
{
Device.BeginInvokeOnMainThread(() =>
{
DirectionMap directionMap = obj as DirectionMap;
var list = new List<Position>(customMap.RouteCoordinates);
foreach (Step step in directionMap.steps)
{
list.Add(directionMap.address_end.position);
System.Diagnostics.Debug.WriteLine("add step");
}
System.Diagnostics.Debug.WriteLine("last step");
customMap.RouteCoordinates = list;
System.Diagnostics.Debug.WriteLine("finished?");
});
}
else
{
System.Diagnostics.Debug.WriteLine(str);
}
}
The map is still doesn't display the polyline :/ I only made these changes, I didn't change anything else from my previous code.
I didn't tell you, but I'm not an expert in MVVM binding, so if I forget something, I'm sorry :/
EDIT 2
So after your answer and some read, read and re-read of your answer, there is my "test code" in MapPage.xaml.cs
public MapPage()
{
InitializeComponent();
//HttpRequest.getDirections(origin, destination, callbackDirections);
Device.BeginInvokeOnMainThread(() =>
{
customMap.RouteCoordinates = new List<Position>
{
new Position (37.797534, -122.401827),
new Position (37.776831, -122.394627)
};
});
//setupMap();
//setupMapCustom();
}
Because it doesn't works (for me), I took a look at my code and then, I saw that public static readonly BindableProperty RouteCoordinatesProperty =
BindableProperty.Create<CustomMap, List<Position>>(
p => p.RouteCoordinates, new List<Position>()); was deprecated..
So I red on this post a different way to implement this binding, but it also said that this way is deprecated SEE HERE... I also saw some tutorials about binding which says that they put some code into their xaml, let me remember you mine
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:NAMESPACE;assembly=NAMESPACE"
x:Class="NAMESPACE.Controlers.MapPage">
<ContentPage.Content>
<local:CustomMap x:Name="customMap"/>
</ContentPage.Content>
</ContentPage>
I'm not using something as ItemSource="{PolylineBindable}"
The custom renderer from the example is not made for dynamic updating the path. It is just implemented for the case, where all points of the paths are known before initializing the map / drawing the path the first time. So you have this race condition, you ran into, because you are loading the directions from a web service.
So you have to do some changes:
RouteCoordinates must be a BindableProperty
public class CustomMap : Map
{
public static readonly BindableProperty RouteCoordinatesProperty =
BindableProperty.Create<CustomMap, List<Position>>(p => p.RouteCoordinates, new List<Position>());
public List<Position> RouteCoordinates
{
get { return (List<Position>)GetValue(RouteCoordinatesProperty); }
set { SetValue(RouteCoordinatesProperty, value); }
}
public CustomMap ()
{
RouteCoordinates = new List<Position>();
}
}
Update the Polyline whenever the coordinates change
Move the creation of the polyline from OnMapReady to UpdatePolyLine
call UpdatePolyLine from OnMapReady and OnElementPropertyChanged
public class CustomMapRenderer : MapRenderer, IOnMapReadyCallback
{
GoogleMap map;
Polyline polyline;
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<View> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
// Unsubscribe
}
if (e.NewElement != null)
{
((MapView)Control).GetMapAsync(this);
}
}
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (this.Element == null || this.Control == null)
return;
if (e.PropertyName == CustomMap.RouteCoordinatesProperty.PropertyName)
{
UpdatePolyLine();
}
}
private void UpdatePolyLine()
{
if (polyline != null)
{
polyline.Remove();
polyline.Dispose();
}
var polylineOptions = new PolylineOptions();
polylineOptions.InvokeColor(0x66FF0000);
foreach (var position in ((CustomMap)this.Element).RouteCoordinates)
{
polylineOptions.Add(new LatLng(position.Latitude, position.Longitude));
}
polyline = map.AddPolyline(polylineOptions);
}
public void OnMapReady(GoogleMap googleMap)
{
map = googleMap;
UpdatePolyLine();
}
}
Setting the data
Updating the positions changes a bit. Instead of adding the positions to the existing list, you have to (create a new list) and set it to RouteCoordinates. You can use Device.BeginInvokeOnMainThread to ensure, that the operation is performed on the UI thread. Else the polyline will not update.
Device.BeginInvokeOnMainThread(() =>
{
customMap.RouteCoordinates = new List<Position>
{
new Position (37.797534, -122.401827),
new Position (37.776831, -122.394627)
};
})
In your case it's something like
var list = new List<Position>(customMap.RouteCoordinates);
list.Add(directionMap.address_end.position);
customMap.RouteCoordinates = list;
Todo
On iOS you have now to implement a similar behavior (like UpdatePolyLine)
Note
That might not the most performant implementation, because you redraw everything instead of adding one point. But it's fine as long as you have no performance issues :)
I followed the tutorial available on Xamarin Docs and it worked for me with some changes based on #Sven-Michael Stübe answer
I load the coordinates from a WebService and then I create a separate List, and after this, I set the new list to the RouteCoordinates property on Custom Map.
Some changes are made on Android Renderer
I'm using MVVM.
CustomMap Class:
public static readonly BindableProperty RouteCoordinatesProperty =
BindableProperty.Create(nameof(RouteCoordinates), typeof(List<Position>), typeof(CustomMap), new List<Position>(), BindingMode.TwoWay);
public List<Position> RouteCoordinates
{
get { return (List<Position>)GetValue(RouteCoordinatesProperty); }
set { SetValue(RouteCoordinatesProperty, value); }
}
public CustomMap()
{
RouteCoordinates = new List<Position>();
}
ViewModel (Codebehind, in your case):
private async void LoadCoordinates(string oidAula, CustomMap mapa)
{
IsBusy = true;
var percurso = await ComunicacaoServidor.GetPercurso(oidAula); // Get coordinates from WebService
var pontos = percurso.Select(p => new Position(p.Latitude, p.Longitude)).ToList(); // Create coordinates list from webservice result
var latitudeMedia = percurso[percurso.Count / 2].Latitude;
var longitudeMedia = percurso[percurso.Count / 2].Longitude;
mapa.RouteCoordinates = pontos;
mapa.MoveToRegion(MapSpan.FromCenterAndRadius(new Position(latitudeMedia, longitudeMedia), Distance.FromMiles(1.0)));
IsBusy = false;
}
XAML:
<maps:CustomMap
AbsoluteLayout.LayoutFlags = "All"
AbsoluteLayout.LayoutBounds = "0, 0, 1, 1"
VerticalOptions = "FillAndExpand"
HorizontalOptions = "FillAndExpand"
x:Name = "PercursoMapa" />
Android Renderer:
public class CustomMapRenderer : MapRenderer
{
bool isDrawn;
protected override void OnElementChanged(ElementChangedEventArgs<Map> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
// Unsubscribe
}
if (e.NewElement != null)
Control.GetMapAsync(this);
}
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if ((e.PropertyName == "RouteCoordinates" || e.PropertyName == "VisibleRegion") && !isDrawn)
{
var polylineOptions = new PolylineOptions();
polylineOptions.InvokeColor(0x66FF0000);
var coordinates = ((CustomMap)Element).RouteCoordinates;
foreach (var position in coordinates)
polylineOptions.Add(new LatLng(position.Latitude, position.Longitude));
NativeMap.AddPolyline(polylineOptions);
isDrawn = coordinates.Count > 0;
}
}
}
This example have more than 3600 points of location and the polyline shows correctly on device:
Screenshot
Building on these answers, here is what I did to get it to work on iOS. This allows changing the route even after the map is loaded, unlike the Xamarin sample.
Firstly, custom map class as per #Sven-Michael Stübe with the update from #Emixam23:
public class CustomMap : Map
{
public static readonly BindableProperty RouteCoordinatesProperty =
BindableProperty.Create(nameof(RouteCoordinates), typeof(List<Position>), typeof(CustomMap), new List<Position>(), BindingMode.TwoWay);
public List<Position> RouteCoordinates
{
get { return (List<Position>)GetValue(RouteCoordinatesProperty); }
set { SetValue(RouteCoordinatesProperty, value); }
}
public CustomMap()
{
RouteCoordinates = new List<Position>();
}
}
Next, the iOS custom renderer:
[assembly: ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer))]
namespace KZNTR.iOS
{
public class CustomMapRenderer : MapRenderer
{
MKPolylineRenderer polylineRenderer;
CustomMap map;
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if ((this.Element == null) || (this.Control == null))
return;
if (e.PropertyName == CustomMap.RouteCoordinatesProperty.PropertyName)
{
map = (CustomMap)sender;
UpdatePolyLine();
}
}
[Foundation.Export("mapView:rendererForOverlay:")]
MKOverlayRenderer GetOverlayRenderer(MKMapView mapView, IMKOverlay overlay)
{
if (polylineRenderer == null)
{
var o = ObjCRuntime.Runtime.GetNSObject(overlay.Handle) as MKPolyline;
polylineRenderer = new MKPolylineRenderer(o);
//polylineRenderer = new MKPolylineRenderer(overlay as MKPolyline);
polylineRenderer.FillColor = UIColor.Blue;
polylineRenderer.StrokeColor = UIColor.Red;
polylineRenderer.LineWidth = 3;
polylineRenderer.Alpha = 0.4f;
}
return polylineRenderer;
}
private void UpdatePolyLine()
{
var nativeMap = Control as MKMapView;
nativeMap.OverlayRenderer = GetOverlayRenderer;
CLLocationCoordinate2D[] coords = new CLLocationCoordinate2D[map.RouteCoordinates.Count];
int index = 0;
foreach (var position in map.RouteCoordinates)
{
coords[index] = new CLLocationCoordinate2D(position.Latitude, position.Longitude);
index++;
}
var routeOverlay = MKPolyline.FromCoordinates(coords);
nativeMap.AddOverlay(routeOverlay);
}
}
}
And finally, adding a polyline to the map:
Device.BeginInvokeOnMainThread(() =>
{
customMap.RouteCoordinates.Clear();
var plist = new List<Position>(customMap.RouteCoordinates);
foreach (var point in track.TrackPoints)
{
plist.Add(new Position(double.Parse(point.Latitude, CultureInfo.InvariantCulture), double.Parse(point.Longitude, CultureInfo.InvariantCulture)));
}
customMap.RouteCoordinates = plist;
var firstpoint = (from pt in track.TrackPoints select pt).FirstOrDefault();
customMap.MoveToRegion(MapSpan.FromCenterAndRadius(new Position(double.Parse(firstpoint.Latitude, CultureInfo.InvariantCulture), double.Parse(firstpoint.Longitude, CultureInfo.InvariantCulture)), Distance.FromMiles(3.0)));
});
Not sure if this is the best way to do it, or the most efficient, I don't know much about renderers, but it does seem to work.
So after lot of searches and, of course, the answer of #Sven-Michael Stübe, you can have your proper maps which works on each platform "Android, iOS, WinPhone". Follow my code, then edit it following the #Sven-Michael Stübe's answer.
Once you finished everything, it could works (like for #Sven-Michael Stübe), but it also couldn't work (like for me). If it doesn't works, try to change the following code:
public static readonly BindableProperty RouteCoordinatesProperty =
BindableProperty.Create<CustomMap, List<Position>>(
p => p.RouteCoordinates, new List<Position>());
by
public static readonly BindableProperty RouteCoordinatesProperty =
BindableProperty.Create(nameof(RouteCoordinates), typeof(List<Position>), typeof(CustomMap), new List<Position>(), BindingMode.TwoWay);
See the documentation for more information about it. (Deprecated implementation)
Then the code works !
PS: You can have some troubles with the polyline at the end, which not following the road right, I'm working on it.
PS2: I'll also make a video to explain how to code your customMap to don't have to install a NuGet package, to be able to edit everything at the end ! (The first one will be in French, the second in English, this post will be edited when the video will be made)
Thank angain to #Sven-Michael Stübe !! Thank to up his answer as well :)