Emixam23 Emixam23 - 19 days ago 5
C# Question

Xamarin C# - Object in List<> auto update themselves, how to update the list from them?

I'm working on xamarin solution for my Github repository. For the Pins project, I'm facing a problem. There is the thing, if you create a pin (aka

CustomPin()
), and then, you want to edit the location. Then the address name will change and same for the location if you change the address, the location will be create based on the address name. So from here, it's easy.

However, when the Pin address/location is changing, I would like my Map update itself. But because the List<> property doesn't changes, it doesn't update the map.

So, get a pointer to your parent list or to the map?

But this solution doesn't seems properly good for my use, I think that maybe, another best solution exist, but I don't know how to do..

You can compile the project, but there is three updates.

CustomPin

public class CustomPin : BindableObject
{
public static readonly BindableProperty AddressProperty =
BindableProperty.Create(nameof(Address), typeof(string), typeof(CustomPin), "",
propertyChanged: OnAddressPropertyChanged);
public string Address
{
get { return (string)GetValue(AddressProperty); }
set { SetValue(AddressProperty, value); }
}
private static async void OnAddressPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
(bindable as CustomPin).SetValue(LocationProperty, await CustomMap.GetAddressPosition(newValue as string));

}

public static readonly BindableProperty LocationProperty =
BindableProperty.Create(nameof(Location), typeof(Position), typeof(CustomPin), new Position(),
propertyChanged: OnLocationPropertyChanged);
public Position Location
{
get { return (Position)GetValue(LocationProperty); }
set { SetValue(LocationProperty, value); }
}
private static async void OnLocationPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
(bindable as CustomPin).SetValue(AddressProperty, await CustomMap.GetAddressName((Position)newValue));
Debug.WriteLine("private static async void OnLocationPropertyChanged(BindableObject bindable, object oldValue, object newValue)");
}

public string Name { get; set; }
public string Details { get; set; }
public string ImagePath { get; set; }
public uint PinSize { get; set; }
public uint PinZoomVisibilityMinimumLimit { get; set; }
public uint PinZoomVisibilityMaximumLimit { get; set; }
public Point AnchorPoint { get; set; }
public Action<CustomPin> PinClickedCallback { get; set; }

public CustomPin(Position location)
{
Location = location;
Name = "";
Details = "";
ImagePath = "";
PinSize = 50;
PinZoomVisibilityMinimumLimit = uint.MinValue;
PinZoomVisibilityMaximumLimit = uint.MaxValue;
AnchorPoint = new Point(0.5, 1);
PinClickedCallback = null;
}
public CustomPin(string address)
{
Address = address;
Name = "";
Details = "";
ImagePath = "";
PinSize = 50;
PinZoomVisibilityMinimumLimit = uint.MinValue;
PinZoomVisibilityMaximumLimit = uint.MaxValue;
AnchorPoint = new Point(0.5, 1);
PinClickedCallback = null;
}
public CustomPin()
{
Address = "";
Location = new Position();

Name = "";
Details = "";
ImagePath = "";
PinSize = 50;
PinZoomVisibilityMinimumLimit = uint.MinValue;
PinZoomVisibilityMaximumLimit = uint.MaxValue;
AnchorPoint = new Point(0.5, 1);
PinClickedCallback = null;
}
}


CustomMap | PS: Just add these three methods

#region
public async static Task<string> GetAddressName(Position position)
{
string url = "https://maps.googleapis.com/maps/api/geocode/json";
string additionnal_URL = "?latlng=" + position.Latitude + "," + position.Longitude
+ "&key=" + App.GOOGLE_MAP_API_KEY;

JObject obj = await CustomMap.GoogleAPIHttpRequest(url, additionnal_URL);

string address_name;
try
{
address_name = (obj["results"][0]["formatted_address"]).ToString();
}
catch (Exception)
{
return ("");
}
return (address_name);
}
public async static Task<Position> GetAddressPosition(string name)
{
string url = "https://maps.googleapis.com/maps/api/geocode/json";
string additionnal_URL = "?address=" + name
+ "&key=" + App.GOOGLE_MAP_API_KEY;

JObject obj = await CustomMap.GoogleAPIHttpRequest(url, additionnal_URL);

Position position;
try
{
position = new Position(Double.Parse((obj["results"][0]["geometry"]["location"]["lat"]).ToString()),
Double.Parse((obj["results"][0]["geometry"]["location"]["lng"]).ToString()));
}
catch (Exception)
{
position = new Position();
}
return (position);
}

private static async Task<JObject> GoogleAPIHttpRequest(string url, string additionnal_URL)
{
try
{
var client = new HttpClient();
client.BaseAddress = new Uri(url);

var content = new StringContent("{}", Encoding.UTF8, "application/json");
HttpResponseMessage response = null;
try
{
response = await client.PostAsync(additionnal_URL, content);
}
catch (Exception)
{
return (null);
}
string result = await response.Content.ReadAsStringAsync();
if (result != null)
{
try
{
return JObject.Parse(result);
}
catch (Exception)
{
return (null);
}
}
else
{
return (null);
}
}
catch (Exception)
{
return (null);
}
}
#endregion


MainPage.xaml.cs | PS: PCL part, just change the Constructor

public MainPage()
{
base.BindingContext = this;

CustomPins = new List<CustomPin>()
{
new CustomPin("Long Beach") { Name = "Le Mans", Details = "Famous city for race driver !", ImagePath = "CustomIconImage.png", PinZoomVisibilityMinimumLimit = 0, PinZoomVisibilityMaximumLimit = 150, PinSize = 75},
new CustomPin() { Name = "Ruaudin", Details = "Where I'm coming from.", ImagePath = "CustomIconImage.png", PinZoomVisibilityMinimumLimit = 75, PinSize = 65 },
new CustomPin() { Name = "Chelles", Details = "Someone there.", ImagePath = "CustomIconImage.png", PinZoomVisibilityMinimumLimit = 50, PinSize = 70 },
new CustomPin() { Name = "Lille", Details = "Le nord..", ImagePath = "CustomIconImage.png", PinZoomVisibilityMinimumLimit = 44, PinSize = 40 },
new CustomPin() { Name = "Limoges", Details = "I have been there ! :o", ImagePath = "CustomIconImage.png", PinZoomVisibilityMinimumLimit = 65, PinSize = 20 },
new CustomPin() { Name = "Douarnenez", Details = "A trip..", ImagePath = "CustomIconImage.png", PinZoomVisibilityMinimumLimit = 110, PinSize = 50 }
};

Debug.WriteLine("Initialization done.");

PinActionClicked = PinClickedCallback;

PinsSize = Convert.ToUInt32(100);

MinValue = 50;
MaxValue = 100;

InitializeComponent();
Debug.WriteLine("Components done.");
}





Maybe it's easy or maybe the way I said it's the only one, but I don't know how to update the pin on the map if a pin got edited, because finaly, it's still the same object, so the list doesn't change...

Thank for help !

Edit 1



Ok so, I made some changes however, it still doesn't work.. I mean my code work as I want, but calling PropertyChanged doesn't change anything...

I changed some things like
List<CustomPin>
is now
ObservableCollection<CustomPin>
.. Also I changed a bit the xaml part to that:

<control:CustomMap x:Name="MapTest" CustomPins="{Binding CustomPins}" CameraFocusParameter="OnPins"
PinSize="{Binding PinsSize, Converter={StaticResource Uint}}"
PinClickedCallback="{Binding PinActionClicked}"
VerticalOptions="Fill" HorizontalOptions="Fill"/>


And my
CustomPin
is now like that:

public class CustomPin : BindableObject, INotifyPropertyChanged
{
/// <summary>
/// Handler for event of updating or changing the
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;

public static readonly BindableProperty AddressProperty =
BindableProperty.Create(nameof(Address), typeof(string), typeof(CustomPin), "",
propertyChanged: OnAddressPropertyChanged);
public string Address
{
get { return (string)GetValue(AddressProperty); }
set { SetValue(AddressProperty, value); }
}
private static void OnAddressPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
(bindable as CustomPin).SetAddress(newValue as string);
Debug.WriteLine("Address property changed");
}
private async void SetAddress(string address)
{
if (setter == SetFrom.None)
{
setter = SetFrom.Address;
SetLocation(await CustomMap.GetAddressPosition(address));
setter = SetFrom.None;
NotifyChanges();
}
else if (setter == SetFrom.Location)
{
setter = SetFrom.Done;
SetValue(AddressProperty, address);
}
}

private enum SetFrom
{
Address,
Done,
Location,
None,
}
private SetFrom setter;

private async void SetLocation(Position location)
{
if (setter == SetFrom.None)
{
setter = SetFrom.Location;
SetAddress(await CustomMap.GetAddressName(location));
setter = SetFrom.None;
NotifyChanges();
}
else if (setter == SetFrom.Address)
{
setter = SetFrom.Done;
SetValue(LocationProperty, location);
}
}

public static readonly BindableProperty LocationProperty =
BindableProperty.Create(nameof(Location), typeof(Position), typeof(CustomPin), new Position(),
propertyChanged: OnLocationPropertyChanged);
public Position Location
{
get { return (Position)GetValue(LocationProperty); }
set { SetValue(LocationProperty, value); }
}
private static async void OnLocationPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
(bindable as CustomPin).SetLocation((Position)newValue);
Debug.WriteLine("Location property changed");
}

private void NotifyChanges()
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Address)));
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Location)));
}

public string Name { get; set; }
public string Details { get; set; }
public string ImagePath { get; set; }
public uint PinSize { get; set; }
public uint PinZoomVisibilityMinimumLimit { get; set; }
public uint PinZoomVisibilityMaximumLimit { get; set; }
public Point AnchorPoint { get; set; }
public Action<CustomPin> PinClickedCallback { get; set; }

public CustomPin(Position location)
{
setter = SetFrom.None;
Location = location;
Name = "";
Details = "";
ImagePath = "";
PinSize = 50;
PinZoomVisibilityMinimumLimit = uint.MinValue;
PinZoomVisibilityMaximumLimit = uint.MaxValue;
AnchorPoint = new Point(0.5, 1);
PinClickedCallback = null;
}
public CustomPin(string address)
{
setter = SetFrom.None;
Address = address;
Name = "";
Details = "";
ImagePath = "";
PinSize = 50;
PinZoomVisibilityMinimumLimit = uint.MinValue;
PinZoomVisibilityMaximumLimit = uint.MaxValue;
AnchorPoint = new Point(0.5, 1);
PinClickedCallback = null;
}
public CustomPin()
{
setter = SetFrom.None;
Address = "";
Location = new Position();
Name = "";
Details = "";
ImagePath = "";
PinSize = 50;
PinZoomVisibilityMinimumLimit = uint.MinValue;
PinZoomVisibilityMaximumLimit = uint.MaxValue;
AnchorPoint = new Point(0.5, 1);
PinClickedCallback = null;
}
}


Finally, a call of
PropertyChanged
doesn't do anything.. Any idea?

Thank !

PS: Do not forget that the solution is available on my github repository

Answer

I finally had an idea ! In Xamarin Forms, App can be acceeded from anywhere so, what I did is:

  • Create a method in your MainPage.xaml.cs which invoke a PropertyChanged event. You can do something like that so:

    public void PinsCollectionChanged()
    {
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CustomPins)));
        Debug.WriteLine("Updated !!!");
    }
    
  • Then, from your item (For me it's a CustomPin object) of your list, call this method by getting the current instance of the application. Take a look at the code to understand:

    private void NotifyChanges()
    {
        (App.Current.MainPage as MainPage).PinsCollectionChanged();
    }
    

PS: Do not forget to add using MapPinsProject.Page; in your object.

Hope it helps !