I am working on a UWP navigation app.
What I want to do?
I want to draw multiple (10 to be exact) polylines on the Map control.
I want one to be different color, while the others are grey.
Once either of the polyline is selected the new selected polyline becomes of the primary color while the others are greyed out. Something like the Maps app for multiple routes just on a larger scale that shows movements of shipping trucks.
Everywhere online there are ways to implement the polyline via c# or code behind, but I want to do it via XAML as adding 10 polylines from codebehind doesn't give much flexibility with events and opacity and Tags and Names.
What all have I tried:
I tried creating an attached polyline to the mapElement but the issue with the approach is that I'll have to each time remove and recreate the polyline to change colors. More about this here. Also, it's just a pretty way of implementing polyline from code behind.
What am I doing currently:
I added the DataTemplate of the PolyLine in my Page.Resources like below:
<DataTemplate x:Key="PolylineDataTemplate" x:DataType="polyLineData:IPolylinePath">
<Polyline Points="{x:Bind Polyline,Mode=OneWay}" Fill="{x:Bind PolylineColor,Mode=OneWay}" Tag="{x:Bind PolylineTag,Mode=OneWay}" StrokeThickness="{x:Bind PolylineThinkness}" />
</DataTemplate>`
Where the IPolylinePath is defined as:
public interface IPolylinePath
{
SolidColorBrush PolylineColor { get; set; }
int PolylineThinkness { get; set; }
string PolylineTag { get; set; }
IEnumerable<IBasicGeoposition> PolylinePoints { get; set; }
Geopath PolylineGeopath { get; }
PointCollection Polyline { get; }
}`
My Polyline property is populated as below:
public PointCollection Polyline
{
get
{
PointCollection returnObject = new PointCollection();
//could have used LINQ but wanted to check if the collection is being populated correctly
foreach (var location in PolylinePoints)
{
returnObject.Add(new Windows.Foundation.Point(location.Latitude, location.Longitude));
}
return returnObject;
}
}
And I am just calling it in the MapItems control like below:
<maps:MapControl x:Name="MyMap" >
<maps:MapItemsControl ItemTemplate="{StaticResource PolylineDataTemplate}" ItemsSource="{x:Bind ViewModel.PolylinePoints}"/>
</maps:MapControl>
The Issue is:
The code works perfectly well. Just the polyline is not visible.
I thought that it's just small that's why I can't see it. So I increased the size and the distance and it just appears as a small arc on the top left corner (with some spacing) and doesn't get scoped or panned.
Can anyone please help?
Just the polyline is not visible.
Firstly, it seems like you didn't give a Stoke property for the Polyline, by default it is null. Your code snippet set color with Fill property it is not for the color of the line, you may find the value of StrokeThickness has no influence with the Polyline and a straight line will not been seen without the Stroke property. So here the color should be bind to Stroke property.
it just appears as a small arc on the top left corner
It is because you build points of the Points property for the Polyline by code line new Windows.Foundation.Point(location.Latitude, location.Longitude).
The latitude and longitude defined the element location on the MapControl not the application view. In another words, actually you add a GeoPoint to the PointCollection, not a Point. So you may need to transfer the GeoPoint to Point by GetOffsetFromLocation(Geopoint, Point) method.
doesn't get scoped or panned.
For this, the Polyline is actually a shape not a MapElement. You should control its MapLocation by listening the map zoom events. If you want it be pan with the map, you should use Map​Polyline. For sample of this please reference the scenario 2 of the official sample. But MapPolyline cannot be added directly by binding, only code behind.
A completed simple sample based on yours for testing as follows:
XAML
<Page.Resources>
<DataTemplate x:Key="PolylineDataTemplate" x:DataType="local:PolylinePath">
<Polyline
Points="{x:Bind Polyline}"
Stroke="{x:Bind PolylineColor}"
StrokeThickness="{x:Bind PolylineThinkness}"
Tag="{x:Bind PolylineTag}" />
</DataTemplate>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<maps:MapControl x:Name="MyMap" Loaded="MyMap_Loaded">
<maps:MapItemsControl x:Name="mapitems" ItemTemplate="{StaticResource PolylineDataTemplate}" />
</maps:MapControl>
<Button
x:Name="btnaddpolyline"
Click="btnaddpolyline_Click"
Content="add" />
</Grid>
Code behind:
public sealed partial class MainPage : Page
{
public List<PolylinePath> polylines { get; set; }
Geopoint SeattleGeopoint = new Geopoint(new BasicGeoposition() { Latitude = 47.604, Longitude = -122.329 });
public MainPage()
{
this.InitializeComponent();
}
private void MyMap_Loaded(object sender, RoutedEventArgs e)
{
MyMap.Center = SeattleGeopoint;
MyMap.ZoomLevel = 16;
}
private void btnaddpolyline_Click(object sender, RoutedEventArgs e)
{
polylines = new List<PolylinePath>()
{
new PolylinePath(MyMap)
{
PolylineColor=new SolidColorBrush(Colors.Red),
PolylineThinkness=3,
PolylineTag="testing",
PolylinePoints = new List<BasicGeoposition>()
{
SeattleGeopoint.Position,
new BasicGeoposition()
{
Latitude = SeattleGeopoint.Position.Latitude + 0.003,
Longitude = SeattleGeopoint.Position.Longitude - 0.003
}
}
}
};
mapitems.ItemsSource = polylines;
}
}
public class PolylinePath
{
public PolylinePath(MapControl MyMap)
{
this.MyMap = MyMap;
}
MapControl MyMap;
public SolidColorBrush PolylineColor { get; set; }
public int PolylineThinkness { get; set; }
public string PolylineTag { get; set; }
public IEnumerable<BasicGeoposition> PolylinePoints { get; set; }
public PointCollection Polyline
{
get
{
PointCollection returnObject = new PointCollection();
//could have used LINQ but wanted to check if the collection is being populated correctly
foreach (var location in PolylinePoints)
{
Point actualpoint;
MyMap.GetOffsetFromLocation(new Geopoint(location), out actualpoint);
returnObject.Add(actualpoint);
}
return returnObject;
}
}
}
Related
I'm attempting to get my head around MVVM with XamarinForms and I'm slightly confused with regards to proper partitioning of functionality:
I have a main page, MainPage.xaml, which includes a stacklayout:
<StackLayout x:Name="MainPageStackLayout">
...
</StackLayout>
Within this stacklayout I have Picker which is bound as follows:
<Picker Title="Select a background colour"
TitleColor="Black"
TextColor="Black"
ItemsSource="{Binding MyColours}"
ItemDisplayBinding="{Binding Name}"
SelectedItem="{Binding selectedBackGroundColour}" SelectedIndexChanged="BackGroundColourPicker_SelectedIndexChanged"/>
Following the article from microsoft (https://learn.microsoft.com/en-us/samples/xamarin/xamarin-forms-samples/userinterface-monkeyapppicker/):
I have a "View" which basically defines the layout of my page.
A "ViewModel" which holds an IList "MyColours" and a variable "SelectedBackGroundColour".
A "Model" which defines the MyColour class. A MyColour has a string name and a Xamarin.Forms.Color (from a hex value, both populated on start up).
This all works fine. I can start up the app and the Picker populates with the colours I add to "MyColours". If I change the index then my SelectedBackGroundColour also updates, has the correct name and a different RGB value.
However, I'm lost as to where I would tie in the updating of the actual background colour of the MainPageStackLayout. The View (MainPage.xaml.cs) picks up the "BackGroundColourPicker_SelectedIndexChanged" event but what is the standard practice for reading from the view model (where SelectedBackGround colour is actual defined ?)
I have a feeling I can bind Background colour in the MainPageStackLayout xaml view so I wont have to catch the selected index change event.
Thanks all.
According to your description, I guess that you want to change MainPage StackLayout BackGround color by Picker value, am I right?
If yes, please follow the steps below.
Firstly, please confirm that you implement INotifyPropertyChanged interface to notify SelectedBackGroundColour changed.
Then there are full code, please take a look:
<StackLayout x:Name="MainPageStacklayout" BackgroundColor="{Binding selectedBackGroundColour.color}">
<Picker
x:Name="picker1"
Title="Select a background colour"
ItemDisplayBinding="{Binding name}"
ItemsSource="{Binding MyColours}"
SelectedItem="{Binding selectedBackGroundColour}"
TextColor="Black"
TitleColor="Black" />
</StackLayout>
public partial class Page5 : ContentPage, INotifyPropertyChanged
{
public ObservableCollection<MyColour> MyColours { get; set; }
private MyColour _selectedBackGroundColour;
public MyColour selectedBackGroundColour
{
get { return _selectedBackGroundColour; }
set
{
_selectedBackGroundColour = value;
RaisePropertyChanged("selectedBackGroundColour");
}
}
public Page5()
{
InitializeComponent();
MyColours = new ObservableCollection<MyColour>()
{
new MyColour(){name="red",color=Color.Red},
new MyColour(){name="gray",color=Color.Gray},
new MyColour(){name="BlueViolet",color=Color.BlueViolet}
};
selectedBackGroundColour = MyColours[0];
this.BindingContext = this;
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
public class MyColour
{
public string name { get; set; }
public Color color { get; set; }
}
The screenshot:
I am working on widows phone 8.1 map based application.I want to know how can I draw a map polyline using MVVM pattern. I've already achieved this using the code behind for first creating the polyline and then adding it. My question is can I define a polyline in the XAML itself and give it a source binding to one of my observable collections of type BasicGeopositions in my viewmodel. If yes then how?
Data to be plotted using polyline:
is a list of BasicGeoposition that contains latitudes and longitudes of all the points I need to connect. I tried this way <Maps:MapPolyline Path="{Binding Trip.PTSPositions}"/> but it didn't work. PTSPositions is a list of BasicGeoposition.
What i want to perform:
I want to
MapPolyline polyLine = new MapPolyline() { StrokeColor = Colors.Blue, StrokeThickness = 5 };
polyLine.Path = new Geopath(Trip.PTSPositions);
MyMap.MapElements.Add(polyLine);
perform the above code behind code in XAML using MVVM where the Trip.PTSPositions would be fetched dynamically and the map polyline would be drawn using data binding.
I searched online a lot. I couldn't find anything that does not use code behind for polyline
Here is the implementation suggested up in the comments.
This is the attached bindable property implementation for MapControl and it stays in the Widows Phone 8.1 project:
public class Polyline
{
public static readonly DependencyProperty PathProperty =
DependencyProperty.RegisterAttached(
"Path",
typeof(IBasicGeoposition[]),
typeof(Polyline),
new PropertyMetadata(null, OnPathChanged));
public static void SetPath(UIElement element, IBasicGeoposition[] value)
{
element.SetValue(PathProperty, value);
}
public static IBasicGeoposition[] GetPath(UIElement element)
{
return (IBasicGeoposition[]) element.GetValue(PathProperty);
}
private static void OnPathChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var mapControl = d as MapControl;
if (mapControl == null)
{
throw new InvalidOperationException(
"Polyline.Track property can only be attached to a MapControl!");
}
mapControl.MapElements.Clear();
mapControl.MapElements.Add(CreateMapPolyline(GetPath(mapControl)));
}
private static MapPolyline CreateMapPolyline(IEnumerable<IBasicGeoposition> track)
{
return new MapPolyline
{
Path = new Geopath(track.Select(x =>
new BasicGeoposition
{
Altitude = x.Altitude,
Latitude = x.Latitude,
Longitude = x.Longitude,
})),
StrokeColor = Colors.Red,
StrokeThickness = 3,
StrokeDashed = false
};
}
}
This interface stays in the PCL, probably close to it's implementation (you'll have to add your custom class implementing the interface):
public interface IBasicGeoposition
{
double Altitude { get; set; }
double Latitude { get; set; }
double Longitude { get; set; }
}
Than in view model you have Trip.PTSPositions which is an array of IBasicGeoposition. And in the view (XAML), you'll have:
<maps:MapControl attached:Polyline.Path="{Binding Trip.PTSPositions}"/>
I have a WinRT app with a number of Users, Projects, Meetings, etc.
I have a main screen, with a main screen view model, which should display CurrentUser and has a ListView bound to CurrentUser.ProjectList.
I initialise CurrentUser in the ViewModel using a UserProvider class that gets all the required information from the database.
My problem then becomes very similar to this: Subscribe to INotifyPropertyChanged for nested (child) objects
I have a user and project model:
public class User
{
public int id { get; set; }
public string ForeName { get; set; }
public string Surname { get; set; }
... etc ...
public ObservableCollection<Project> ProjectList { get; set; }
public ObservableCollection<User> FriendList { get; set; }
... constructor
}
public class Project
{
public String Name { get; set; }
public int Id { get; set; }
public List<User> Users { get; set; }
public List<Meeting> Meetings { get; set; }
.. constructor ...
}
A view model with the following:
class HomeScreenViewModel : INotifyPropertyChanged {
private User _currentUser;
public User CurrentUser
{
get { return this._currentUser; }
set
{
if (Equals(_currentUser, value)) return;
this._currentUser = value;
RaisePropertyChanged("CurrentUser");
}
}
//[field: NonSerialized]
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
... I have a method in this view model that gets the current user
public async Task<bool> GetLoggedInUserAsync()
{
int testId = 0;
CurrentUser = await userProvider.GetCurrentUser(testId);
UserProjects = await userProvider.GetUsersProject(CurrentUser);
CurrentUser.ProjectList = UserProjects;
return true;
}
That is called in the view's loadState
public MainPage()
{
this.InitializeComponent();
addMeeting = new AddMeetingFlyout();
_vm = new HomeScreenViewModel();
this.DataContext = _vm;
}
protected override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)
{
await _vm.GetLoggedInUserAsync()
}
And my bindings in the XAML, for ProjectList and ForeName, for example, are as follows:
<CollectionViewSource
x:Name="projectsViewSource"
Source="{Binding CurrentUser.ProjectList}"/>
...
<ListView
x:Name="projectList"
ItemsSource="{Binding Source={StaticResource projectsViewSource}}"
Grid.Row="1"
SelectionMode="None"
Style="{StaticResource DraggableListView}"
ScrollViewer.VerticalScrollBarVisibility="Visible"
IsItemClickEnabled="True"
>
<ListView.ItemTemplate>
<DataTemplate>
<Button Style="{StaticResource ProjectTileButton}" Content="{Binding Name}" Click="ProjectItem_Click" />
</DataTemplate>
</ListView.ItemTemplate>
<AddDeleteThemeTransition/>
</ListView>
...
<Button ...>
<TextBlock ...">
<Run Text="{Binding CurrentUser.ForeName}" />
</TextBlock>
</Button>
The button content, CurrentUser.ForeName fires an INotifyPropertyChanged event when CurrentUser is first initialised in the viewmodel. This is reflected in the view - but any further changes to CurrentUser.ForeName do not fire any subsequent INotifyPropertyChanged events. The ProjectList is also not displayed in the view and does not fire an INotifyPropertyChanged event even though I know it is there.
I have spent many days looking at implementing INotifyPropertyChanged so that changes to nested child complex objects (such as CurrentUser.ProjectList) will propagate up to the view. At the minute, the only way this happens is if I force a call to
this._currentUser = value;
RaisePropertyChanged("CurrentUser");
which I am testing with a button that calls a method called MakeChange() in the viewmodel
public void MakeChange()
{
User updatedCurrentUser = CurrentUser;
CurrentUser = updatedCurrentUser;
}
This works, so I know for a fact all the data is coming correctly from the database and all is as it should be - one less thing to worry about!
However, I simply cannot get the view to display user projects on page load, or when new projects are added.
I tried implementing this solution: https://gist.github.com/thojaw/705450, however, the WinRT reflection capabilites have changed and I am not sure how to get the following liens to work within the context of my project, as this is beyond me:
//from property
//in _type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
//where _inotifyType.IsAssignableFrom(property.PropertyType)
//select property;
Any help would be greatly appreciated - I honestly thought all I had to do was bind CurrentUser.ProjectList to a ListView.
As you are replacing the entire ObservableCollection itself, then you will also need to introduce another property changed event and backing field for the collection property.
There is a good example of this here
The program works fine and doesn't crashing or something. But data is not showing on the table(datagrid)
Updated version:
View: Userperspective.xaml
I am getting errors in xaml file because of the binding path "Products" is unkown datacontext
<Grid Margin="0,0,0,-20">
<DataGrid Name="Producttable" ItemsSource="{Binding Path=Products}"
HorizontalAlignment="Left" Height="200" Margin="10,44,0,0"
VerticalAlignment="Top" Width="972" />
View: Userperspective.xaml.cs
public partial class Userperspective : Window
{
public Userperspective()
{
InitializeComponent();
DataContext = new ProductViewModel();
}
}
ProductviewModel
private readonly Product _product;
private IBackend _backend;
public ICommand ProductCommand { get; set; }
public IList<Product> Products { get; set; }
public ProductViewModel()
{
_backend = new BackendService();
_product = new Product();
ProductCommand = new ProductCommand(this);
}
public Product Product()
{
return _product;
}
public void LoadProducts()
{
Products = _backend.GetProducts();
RaisePropertyChanged("Products");
}
Productcommand
private readonly ProductViewModel _vm;
public ProductCommand(ProductViewModel vm)
{
this._vm = vm;
}
public void Execute(object parameter)
{
_vm.LoadProducts();
}
BackendService
namespace _blabla
{
class BackendService : IBackend
{
public IList<Product> GetProducts()
{
using (var db = new NORTHWNDEntities())
{
var query = from p in db.Products
select new Product
{
Name = p.ProductName,
};
return query.ToList();
}
}
}
}
Ibackend
namespace _blabla.Commands
{
public interface IBackend
{
IList<Product> GetProducts();
}
}
Seeing as you are new to WPF and MVVM you should break the problem down into something a little more manageable. There is a lot going on in your code; MVVM, commands, database access and some abstraction. Your intentions are sound but it doesn't make solving this problem easy.
With the information you have given I'm not even 100% sure what the problem is but I suspect that it is either the binding or the database access. I will concentrate on demonstrating the binding aspect to you.
Seeing as I don't have access to your database code I have mocked up some classes to help me solve this problem.
Note: The command code is noise so I will remove it from my answer and concentrate on binding to a list of products (you can integrate it with your commanding solution once this is working).
Product
public class Product
{
public string Name { get; set; }
public string Description { get; set; }
public override string ToString()
{
return string.Format("Product: ({0}), {1}", Name, Description);
}
}
BackendService : This basically returns an array of products in lieu of being able to access a database.
class BackendService : IBackend
{
public IList<Product> GetProducts()
{
return new Product[]
{
new Product{ Name = "Laptop", Description = "Dell 17inch laptop" },
new Product{ Name = "Mobile Phone", Description = "iPhone" },
new Product{ Name = "Television", Description = "Samsung 32 inch plasma" },
new Product{ Name = "Car", Description = "Gran Torino" },
new Product{ Name = "Book", Description = "Effective C#" },
};
}
}
I have bound the list of products in the viewModel to a Listbox as I don't have access to the DataGrid but otherwise I have not modified the main window code.
Mainwindow.xaml
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox Margin="5"
ItemsSource="{Binding Path=GetProducts}"/>
</Grid>
Mainwindow.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ProductViewModel();
}
}
Now If I use your viewModel, I get a NullReferenceException which originates from your call to _backend.GetProducts() because you have not instantiated an instance of your BackendService. If I update the constructor like so:
public ProductViewModel()
{
_backend = new BackendService();
_product = new Product();
ProductCommand = new ProductCommand(this);
}
and run the application, the list of products is displayed correctly.
You should be able to integrate the code I have supplied into your project and demonstrate that it is working. When you are happy with this, you should update the BackendService class to call the list of products from your database instead. I would recommend doing this as a matter of course for all bindings that way you know whether it is the binding that isn't working or the database call.
You are trying to execute GetProducts but that is a Property not a method - create a seperate method to load products and
Change your property name to something more meaningful
public IList<Product> Products {get;set;}
Then create a method to load your products
public void LoadProducts()
{
Products = _backend.GetProducts();
//You will need to notify of property change here
OnPropertyChanged("Products");
}
Then bind to Products in your xaml
<Window x:Class="_blabla.View.Userperspective"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="UserPerspective" Height="500" Width="1000">
<Grid Margin="0,0,0,-20">
<DataGrid Name="Producttable" ItemsSource="{Binding Path=Products}"
HorizontalAlignment="Left" Height="200" Margin="10,44,0,0"
VerticalAlignment="Top" Width="972" />
</Grid>
</Window>
Then in your command call LoadProducts
public void Execute(object parameter)
{
_vm.LoadProducts();
}
You will need to implement INotifyPropertyChanged so the UI knows you have changed the Products Property
While I was developing a startscreen for my app using the GridView control, I run into a problem. I have a GridView on my main screen which has a CollectionViewSource set as ItemSource.
For this CollectionViewSource the source is set to an ObservableCollection list. Each GroupViewModel has a ObservableCollection in it. In code the important parts looks like the following:
public class StartPageViewModel : ViewModelBase
{
public ObservableCollection<GroupViewModel> Groups { get; set; }
public CollectionViewSource GroupsCvs { get; set; }
public StartPageViewModel()
{
// fill Groups with some mock data
GroupsCvs.Source = Groups;
GroupsCvs.IsSourceGrouped = true;
}
public void MoveItems(GroupViewModel grp)
{
// add a dummy item
grp.AddRecipe(new ItemViewModel(new Item()) { Id = "123" });
RaisePropertyChanged("GroupsCvs");
RaisePropertyChanged("Groups");
}
}
public class GroupViewModel : ViewModelBase, IEnumerable<ItemViewModel>
{
public ObservableCollection<ItemViewModel> Items { get; set; }
}
View:
public sealed partial class MainPage : LayoutAwarePage
{
private ViewModelLocator locator = new ViewModelLocator();
public MainPage()
{
this.InitializeComponent();
this.DataContext = locator.Main; // returns StartPageViewModel
}
}
XAML part for MainPage, GridView
<GridView ItemsSource="{Binding GroupsCvs.View}" ...
</GridView>
How is it possible to get the UI refreshed when I add an Item to a Group's collection? In my StartPageViewModel I'm adding dummy item to the GroupViewModel and I raise propertychanged, but the Grid remains the same.
I've also tried to fire property changed event in the GroupViewModel class, when the Items collection changes without any luck.
Edit: As I wrote in comments it's possible to refresh with reassigning the source property however this gets the GridView rendered again which is not nice. I'm looking to options which would result in a nicer user experience.
I suppose CollectionViewSource doesn't react to PropertyChanged event. Try reassigning Source to GroupCvs after you modify it. It's not elegant but it should work:
GroupsCvs.Source = Groups;
As a last resort you could create a new instance of ObservableCollection<GroupViewModel> before reassigning it:
Groups = new ObservableCollection<GroupViewModel>(Groups)
GroupsCvs.Source = Groups;
<GridView ItemsSource="{Binding GroupsCvs.View, **BindingMode=TwoWay**}" ...
</GridView>