I am working on an app that I had originally written all in C# (including UI) but now I am rewriting it so that UI is handled by xml. Every part of the app worked fine when it was all in C#, however, now that I have switched UI over to xml, I am getting an error in my ViewModels.
error: 'StartPageViewModel' does not contain a definition for 'Navigation' and no accessible extension method 'Navigation' accepting a first argument of type 'StartPageViewModel' could be found (are you missing a using directive or an assembly reference?)
XML
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="SampleApp.Views.StartPage">
<ContentPage.Content>
<StackLayout>
<Grid VerticalOptions="CenterAndExpand">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button x:Name="OnePlayerGame" Text="One Player Game"
Command="{Binding StartOnePlayerGameCommand}"
Grid.Row="0" Grid.Column="0"
HorizontalOptions="Center"/>
<Button x:Name="TwoPlayerGame" Text="Two Player Game"
Command="{Binding StartTwoPlayerGameCommand}"
Grid.Row="0" Grid.Column="1" HorizontalOptions="Center" />
</Grid>
</StackLayout>
</ContentPage.Content>
Code behind
namespace SampleApp.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class StartPage : ContentPage
{
public StartPage()
{
InitializeComponent();
BindingContext = new StartPageViewModel();
}
}
}
ViewModel code
namespace SampleApp.ViewModels
{
class StartPageViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
var changed = PropertyChanged;
if (changed == null)
return;
changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public Command StartOnePlayerGameCommand { get; set; }
public Command StartTwoPlayerGameCommand { get; set; }
public StartPageViewModel()
{
StartOnePlayerGameCommand = new Command(() => StartOnePlayerGame());
StartTwoPlayerGameCommand = new Command(() => StartTwoPlayerGame());
}
private void StartOnePlayerGame()
{
//the error is here (Navigation has a red squigly under it)
this.Navigation.PushAsync(new Views.OnePlayerPage());
}
private void StartTwoPlayerGame()
{
//the error is here (Navigation has a red squigly under it)
this.Navigation.PushAsync(new Views.NameEntryPage());
}
}
}
Navigation is a property of Page and you can pass the Navigation through the ViewModel Constructor, then use it in the ViewModel:
class StartPageViewModel : INotifyPropertyChanged
{
public INavigation myNavigation { get; set; }
public StartPageViewModel(INavigation navigation)
{
myNavigation = navigation;
StartOnePlayerGameCommand = new Command(() => StartOnePlayerGame());
StartTwoPlayerGameCommand = new Command(() => StartTwoPlayerGame());
}
private void StartOnePlayerGame()
{
//the error is here (Navigation has a red squigly under it)
myNavigation.PushAsync(new Views.OnePlayerPage());
}
private void StartTwoPlayerGame()
{
//the error is here (Navigation has a red squigly under it)
myNavigation.PushAsync(new Views.NameEntryPage());
}
}
Pass it from Page:
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
this.BindingContext = new StartPageViewModel(Navigation);
}
}
Another way it that you can try to use Application.Current.MainPage.Navigation:
private async void StartOnePlayerGame()
{
//the error is here (Navigation has a red squigly under it)
await Application.Current.MainPage.Navigation.PushAsync(new Views.OnePlayerPage());
}
Related
I'm new to Xamarin.Forms development and I'm trying to build a tiny Xamarin app to start.
My app currently has one main TabbedPage that has two ContentPages children. On the ListePage, I have a ListView with an ObservableCollection with OlonaModel is an object with an int Numero and a Text string. I would like the Details page to show details of the selected OlonaModel from the listview of ListePage, but the Details page doesn't seem to update to changes when I select an item from the listview.
Both of the content pages are bound to the same ListPageViewModel. The view model updates when I select an item from the listview, but the changes aren't reflected on the Details page and I'm really confused.
How can I make the Details refresh itself when the SelectedItem of the view model gets set ?
The MainPage :
<?xml version="1.0" encoding="utf-8" ?>
<TabbedPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:App.Views"
xmlns:viewmodels="clr-namespace:App.ViewModels"
mc:Ignorable="d"
x:Class="App.MainPage">
<TabbedPage.Children>
<local:ListePage/>
<local:Details/>
</TabbedPage.Children>
</TabbedPage>
The ListePage.xaml (ContentPage1 in the post) :
<?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:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="App.Views.ListePage"
Title="Liste">
<ContentPage.Content>
<StackLayout>
<ListView x:Name="ListeMipoitra" ItemSelected="ListeMipoitra_ItemSelected">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Label Text="{Binding Numero}" Grid.Column="0" FontSize="Medium" FontAttributes="Bold"/>
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage.Content>
</ContentPage>
The ListePage.xaml.cs :
namespace App.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class ListePage : ContentPage
{
public static ListePageViewModel viewModel;
public ListePage()
{
InitializeComponent();
viewModel = new ListePageViewModel();
this.BindingContext = viewModel;
ListeMipoitra.ItemsSource = viewModel.listeOlonaVM;
}
private void ListeMipoitra_ItemSelected(object sender, SelectedItemChangedEventArgs e)
{
viewModel.setSelected((OlonaModel)e.SelectedItem);
}
}
}
The Details.xaml (ContentPage2 in the post) :
<?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:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="App.Views.Details"
Title="Détails">
<ContentPage.Content>
<StackLayout>
<Label Text="{Binding Text}"/>
</StackLayout>
</ContentPage.Content>
</ContentPage>
The Details.xaml.cs :
namespace App.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class Details : ContentPage
{
public Details()
{
InitializeComponent();
this.BindingContext = ListePage.viewModel.selected;
}
}
}
The ListePageViewModel.cs :
Note: I'm using Fody and PropertyChanged.Fody weaver, so the RaisePropertyChanged() event (should be) is called automatically when a property changes
namespace App.ViewModels
{
public class ListePageViewModel : INotifyPropertyChanged
{
public ObservableCollection<OlonaModel> listeOlonaVM;
public OlonaModel selected { get; set; }
public ListePageViewModel()
{
listeOlonaVM = new ObservableCollection<OlonaModel>();
listeOlonaVM = ListeOlona.liste;
}
public event PropertyChangedEventHandler PropertyChanged;
public void setSelected(OlonaModel olona)
{
selected = olona;
}
}
}
The model of the Olona object :
namespace App.Models
{
public class OlonaModel
{
public int Numero { get; set; }
public string Text { get; set; }
public OlonaModel(int num, string text)
{
this.Numero = num;
this.Text= text;
}
}
}
The ListeOlona.cs where the model of the list is stored:
The InitializeList() method is called at App Startup.
namespace App.ViewModels
{
public static class ListeOlona
{
public static ObservableCollection<OlonaModel> liste = new ObservableCollection<OlonaModel>();
public static void InitializeList()
{
liste.Add(new OlonaModel(1,
"FirstItem"));
liste.Add(new OlonaModel(2,
"Second Item"));
}
}
}
According to your description, there is tabbedpage, two pages, one is Listpage, another is detailpage, you want to select listview item in listpage, then other detail info will display detailpage, am I right?
If yes, I suggest you can do this like this:
TabbedPage:
<TabbedPage
x:Class="App4.TabbedPage2"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:App4"
x:Name="tabbedpage2">
<!-- Pages can be added as references or inline -->
<TabbedPage.Children>
<local:ListPage Title="Mian page" BindingContext="{Binding}" />
<local:DetailPage Title="Detail page" BindingContext="{Binding select}" />
</TabbedPage.Children>
public partial class TabbedPage2 : TabbedPage
{
public TabbedPage2 ()
{
InitializeComponent();
this.BindingContext = new ListePageViewModel();
}
}
public class OlonaModel
{
public int Numero { get; set; }
public string Text { get; set; }
}
public class ListePageViewModel : ViewModelBase
{
public ObservableCollection<OlonaModel> listeOlonaVM { get; set; }
private OlonaModel _select;
public OlonaModel select
{
get { return _select; }
set
{
_select = value;
RaisePropertyChanged("select");
}
}
public ListePageViewModel()
{
listeOlonaVM = new ObservableCollection<OlonaModel>()
{
new OlonaModel(){Numero=1,Text="first item"},
new OlonaModel(){Numero=2,Text="second item"},
new OlonaModel(){Numero=3,Text="third item"},
new OlonaModel(){Numero=4,Text="fouth item"}
};
select = listeOlonaVM[0];
}
}
ListPage:
<StackLayout>
<ListView ItemsSource="{Binding listeOlonaVM}" SelectedItem="{Binding select}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout>
<Label Text="{Binding Numero}" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
DetailPage:
<StackLayout>
<Label
HorizontalOptions="CenterAndExpand"
Text="{Binding Text}"
VerticalOptions="CenterAndExpand" />
</StackLayout>
ViewModelBase implement INotifyPropertyChanged interface.
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
- the red markups are for Inputs(xAxis and yAxis)
- after i press the button (Calculate), the inputs should be calculated.
- then outputed at the blue markup(texbox called Output)
After i made some kata with element binding, i just wanted to
start MVVM property binding.
For some reason i get a ErrorCode: CS1061
Error CS1061 MainWindow does not contain a definition for
CalculateClick and no extension method CalculateClick accepting a
first argument of type MainWindow could be found (are you missing a
using directive or an assembly reference?)
the weird part of this is when i use the resharper eventhandler on my MainWindow.xml at my button it creates a event in my MainWindow.cs.
But it didnt before. the events were autocreated in my ViewModel.cs
im not sure what causes this error
ty in advance when somone could help me, i have already been sitting on this kata for more then 8 hours.
Heres my MainWindow.xml:
<Window x:Class="Coordinates.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Coordinates"
mc:Ignorable="d"
Title="MainWindow" Height="250" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="Insert X-Coordiante"/>
<Label Grid.Row="0" Grid.Column="1" Content="Insert Y-Coordinate"/>
<TextBox Grid.Row="1" Grid.Column="0" Name="TxtXaxis" Text="{Binding Xaxis}"/>
<TextBox Grid.Row="1" Grid.Column="1" Name="TxtYaxis" Text="{Binding Xaxis}"/>
<TextBox Grid.Row="2" Grid.Column="0" Text="{Binding Output}"/>
<Button Grid.Row="2" Grid.Column="1" Name="Calculate" Click="CalculateClick">Calculate</Button>
</Grid>
This is my MainWinow.xml.cs:
using System.Windows;
namespace Coordinates
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.InitializeComponent();
this.DataContext = new ViewModel();
}
}
}
This is my ViewModel.cs:
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
namespace Coordinates
{
public class ViewModel : INotifyPropertyChanged
{
#region Inputs
private double _xaxis;
public double Xaxis
{
get => this._xaxis;
set
{
if (value == this._xaxis)
{
return;
}
this._xaxis = value;
this.OnPropertyChanged();
}
}
private double _yaxis;
public double Yaxis
{
get => this._yaxis;
set
{
if (value == this._yaxis)
{
return;
}
this._yaxis = value;
this.OnPropertyChanged();
}
}
#endregion
public void CalculateClick(object sender, RoutedEventArgs e)
{
Output = Math.Sqrt(Math.Pow(Xaxis,2)+Math.Pow(Yaxis,2));
}
private double _output;
public double Output
{
get => this._output;
set
{
if (value == this._output)
{
return;
}
this._output = value;
this.OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
EDIT: this was missing in CalculateClick in my viewmodel:
Output = Math.Sqrt(Math.Pow(Xaxis,2)+Math.Pow(Yaxis,2));
You are using a CalculateClick as Eventhandler for the Click Event of the Button.
Per Default, WPF looks in the Code Behind (MainWindow.cs) for the Eventhandler, which is not there.
Since you are already initializing your DataContext within the Code Behind. Make your ViewModel a private member of your MainWindow.
Create an event handler in the MainWindow Code Behind and call your event handler in the ViewModel from there.
public partial class MainWindow : Window
{
private _viewModel = new ViewModel();
public MainWindow()
{
InitializeComponent();
this.InitializeComponent();
this.DataContext = _viewModel;
}
}
public void CalculateClick(object sender, RoutedEventArgs e)
{
_viewModel.CalculateClick(Sender, e);
}
You might also want to look into Commands, which can be created directly inside of the ViewModel and then be bound to. There is an ActionCommand or GenericCommand, which take an Action or Action<T> and are pretty easy to use.
I been following this Walkthrough which does a great job of explaining switching between two views, plus more.
What I'm trying to adapt the project to do is, instead of switching between two views, is show two views side by side.
Andy set up the following in his MainWindowViewModel placing ViewModels into an OC:
public class MainWindowViewModel : NotifyUIBase
{
public ObservableCollection<ViewVM> Views {get;set;}
public MainWindowViewModel()
{
ObservableCollection<ViewVM> views = new ObservableCollection<ViewVM>
{
new ViewVM{ ViewDisplay="Customers", ViewType = typeof(CustomersView), ViewModelType = typeof(CustomersViewModel)},
new ViewVM{ ViewDisplay="Products", ViewType = typeof(ProductsView), ViewModelType = typeof(ProductsViewModel)}
};
Views = views;
RaisePropertyChanged("Views");
views[0].NavigateExecute();
}
}
In MainWindow.xaml.cs navigation calls ShowUserControl() to set the view
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// Apply default form level font style
Style = (Style)FindResource(typeof(Window));
Messenger.Default.Register<NavigateMessage>(this, (action) => ShowUserControl(action));
this.DataContext = new MainWindowViewModel();
}
private void ShowUserControl(NavigateMessage nm)
{
EditFrame.Content = nm.View;
}
}
My Code:
I won't be needing them in an OC and I won't be switching between views, they will be displayed at the same time side-by-side. So I was thinking what I need to do is
public class MainWindowViewModel : NotifyUIBase
{
private ViewVM m_MobileDeviceRequestsVM;
private ViewVM m_AuthorizedMobileDevicesVM;
public ViewVM MobileDeviceRequestsVM
{
get { return m_MobileDeviceRequestsVM; }
}
public ViewVM AuthorizedMobileDevicesVM
{
get { return m_AuthorizedMobileDevicesVM; }
}
public MainWindowViewModel()
{
m_MobileDeviceRequestsVM = new ViewVM { ViewDisplay = "MobileDeviceRequests", ViewType = typeof(MobileDeviceRequestsView), ViewModelType = typeof(MobileDeviceRequestsViewModel) };
m_AuthorizedMobileDevicesVM = new ViewVM { ViewDisplay = "AuthorizedMobileDevices", ViewType = typeof(AuthorizedMobileDevicesView), ViewModelType = typeof(AuthorizedMobileDevicesViewModel) };
}
}
The problem I'm facing is how to bind these ViewModel Views in to my grid, tried using a couple of ContentControl however that's not working.
How can I accomplish this?
<Window x:Class="MobileDeviceAuthenticator.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MobileDeviceAuthenticator"
Title="Device Authorization" Height="381" Width="879">
<Grid>
<Grid Margin="0,25,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="Authorized Devices" Grid.Row="0" Grid.Column="0" HorizontalAlignment="Left" Margin="20,10,0,0" VerticalAlignment="Top" />
<ContentControl Grid.Row="1" Grid.Column="0" Content="{Binding AuthorizedMobileDevicesVM.View}" />
<Label Content="Device Requests" Grid.Row="0" Grid.Column="1" HorizontalAlignment="Left" Margin="20,10,0,0" VerticalAlignment="Top" />
<ContentControl Grid.Row="1" Grid.Column="1" Content="{Binding MobileDeviceRequestsVM.View}" />
</Grid>
</Grid>
</Window>
I looked at the example's ViewVM class again after I made my comment regarding my reservations about the approach. Ignoring any of that and assuming you have not modified the example's ViewVM code below:
public class ViewVM
{
public string ViewDisplay { get; set; }
public Type ViewType { get; set; }
public Type ViewModelType { get; set; }
public UserControl View { get; set; }
public RelayCommand Navigate { get; set; }
public ViewVM()
{
Navigate = new RelayCommand(NavigateExecute);
}
public void NavigateExecute()
{
if(View == null && ViewType != null)
{
View = (UserControl)Activator.CreateInstance(ViewType);
}
var msg = new NavigateMessage { View = View, ViewModelType = ViewModelType, ViewType = ViewType };
Messenger.Default.Send<NavigateMessage>(msg);
}
}
The issue is that the View property is only assigned to via reflection when NavigateExecute is called. When you bind to AuthorizedMobileDevicesVM.View, it's not instantiated yet. You can move the reflection code into the constructor for your case and it'll work. Of course this means it'll increase memory usage of your application if you're using ViewVM elsewhere for page navigation - looks like it's by design meant to create the view only as necessary.
So I've been struggling with getting my TreeViews to update properly for a long time now and so I'm asking if anyone can tell me why my code isn't properly updating my TreeView nodes on additions or subtractions. I apologize in advance for the somewhat massive code dump but I felt it was all important to illustrate the problem.
For starters my ObservableObject class
public abstract class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
TreeNodeBase Class
public abstract class TreeNodeBase : ObservableObject
{
protected const string ChildNodesPropertyName = "ChildNodes";
protected string name;
public string Name
{
get
{
return this.name;
}
set
{
this.name = value;
this.OnPropertyChanged();
}
}
protected IList<TreeNode> childNodes;
protected TreeNodeBase(string name)
{
this.Name = name;
this.childNodes = new List<TreeNode>();
}
public IEnumerable<TreeNode> ChildNodes
{
get
{
return this.childNodes;
}
}
public TreeNodeBase AddChildNode(string name)
{
var treeNode = new TreeNode(this, name);
this.childNodes.Add(treeNode);
this.OnPropertyChanged(ChildNodesPropertyName);
return treeNode;
}
public TreeNode RemoveChildNode(string name)
{
var nodeToRemove = this.childNodes.FirstOrDefault(node => node.Name.Equals(name));
if (nodeToRemove != null)
{
this.childNodes.Remove(nodeToRemove);
this.OnPropertyChanged(ChildNodesPropertyName);
}
return nodeToRemove;
}
}
public class TreeNode : TreeNodeBase
{
public TreeNodeBase Parent { get; protected set; }
public TreeNode(TreeNodeBase parent, string name)
: base(name)
{
this.Parent = parent;
}
}
The TreeNodeRoot class
public class TreeViewRoot : TreeNodeBase
{
public TreeViewRoot(string name)
: base(name)
{
}
}
The TreeNode Class
public class TreeNode : TreeNodeBase
{
public TreeNodeBase Parent { get; protected set; }
public TreeNode(TreeNodeBase parent, string name)
: base(name)
{
this.Parent = parent;
}
}
The TreeView UserControl Xaml
<UserControl x:Class="TreeViewExperiment.TreeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:treeViewExperiment="clr-namespace:TreeViewExperiment"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400"
d:DataContext="{d:DesignInstance treeViewExperiment:TreeViewmodel}">
<UserControl.DataContext>
<treeViewExperiment:TreeViewmodel/>
</UserControl.DataContext>
<Grid Background="White">
<Grid.Resources>
<HierarchicalDataTemplate x:Key="TreeViewHierarchicalTemplate" ItemsSource="{Binding ChildNodes}">
<TextBlock Text="{Binding Name}"/>
</HierarchicalDataTemplate>
<Style TargetType="Button">
<Setter Property="FontFamily" Value="Verdana"/>
<Setter Property="FontWeight" Value="Bold"/>
</Style>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="6*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TreeView Grid.Row="0" x:Name="Tree" ItemsSource="{Binding RootLevelNodes}" ItemTemplate="{StaticResource TreeViewHierarchicalTemplate}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectedItemChanged">
<i:InvokeCommandAction
Command="{Binding SetSelectedNode}"
CommandParameter="{Binding SelectedItem, ElementName=Tree}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</TreeView>
<Grid Grid.Row="1" Height="25">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="4*"/>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="NameTextBox" Grid.Column="0" VerticalAlignment="Center" FontFamily="Verdana"/>
<Button Grid.Column="1" Content="Add Node" Command="{Binding AddNode}" CommandParameter="{Binding Text, ElementName=NameTextBox}" Background="Green"/>
<Button Grid.Column="2" Content="Remove Node" Command="{Binding RemoveNode}" Background="Red"/>
</Grid>
</Grid>
</UserControl>
Finally the TreeViewmodel
public class TreeViewmodel : ObservableObject
{
public ICommand SetSelectedNode { get; private set; }
public ICommand AddNode { get; private set; }
public ICommand RemoveNode { get; private set; }
public TreeViewmodel()
{
this.SetSelectedNode = new ParamaterizedDelegateCommand(
node =>
{
this.SelectedTreeNode = (TreeNodeBase)node;
});
this.AddNode = new ParamaterizedDelegateCommand(name => this.SelectedTreeNode.AddChildNode((string)name));
this.RemoveNode = new DelegateCommand(
() =>
{
if (selectedTreeNode.GetType() == typeof(TreeNode))
{
var parent = ((TreeNode)this.SelectedTreeNode).Parent;
parent.RemoveChildNode(this.SelectedTreeNode.Name);
this.SelectedTreeNode = parent;
}
});
var adam = new TreeViewRoot("Adam");
var steve = adam.AddChildNode("Steve");
steve.AddChildNode("Jack");
this.rootLevelNodes = new List<TreeViewRoot> { adam, new TreeViewRoot("Eve") };
}
private TreeNodeBase selectedTreeNode;
private readonly IList<TreeViewRoot> rootLevelNodes;
public IEnumerable<TreeViewRoot> RootLevelNodes
{
get
{
return this.rootLevelNodes;
}
}
public TreeNodeBase SelectedTreeNode
{
get
{
return this.selectedTreeNode;
}
set
{
this.selectedTreeNode = value;
this.OnPropertyChanged();
}
}
}
So I know that the UI should be getting notified when child elements are added removed as when I debug it I can see that the get accessor on the ChildNodes property is called in both cases, yet what is displayed on the UI remains unchanged.
In the past I've gotten around this but using ObservableCollections and that seems to be what most solutions to this sort of problem point to here on StackOverflow, but why doesn't this solution also work? What am I missing?
The problem is that you are misusing INotifyPropertyChanged. In your code you are notifying the view that your property ChildNodes changed but it isn't true as TreeViewItem.ItemsSource still equals your ChildNodes property.
INotifyPropertyChanged will cause UI update when underlying collection object in yout view model changes.
To get ItemsSource updated when new item in collection occurs you need to use a collection which implements INotifyCollectionChanged.
As MSDN says:
You can enumerate over any collection that implements the IEnumerable interface. However, to set up dynamic bindings so that insertions or deletions in the collection update the UI automatically, the collection must implement the INotifyCollectionChanged interface. This interface exposes an event that should be raised whenever the underlying collection changes.
That's why everyone on SO advise to use ObservableCollection.
EDIT:
If you want to expose read-only collection you should check ReadOnlyObservableCollection<T> Class. It works as a wrapper for ObservableCollection which can be made non public.
I'm currently making an app using Xamarin Forms. This app will first call a REST service to retrieve the data and display them then store those data into a SQLite Database. I have an update button where if I click on it, it will prompt the REST service once again to retrieve newer data and replace the old data while the app is running. I have tried to implement the INotifyPropertyChanged but the value just wont' change for me. Am I missing anything with my code below? Thanks!
Vitals Object:
public class Vitals
{
public string Height { get; set; }
public string ID { get; set; }
public string Weight { get; set; }
}
Update Method:
async void OnUpdate(object sender, EventArgs e)
{
string tempUser = globalPatient.Username;
string tempPin = globalPatient.PIN;
patUpdate = patientManager.GetPatientByUsername (tempUser, tempPin).Result;
App.PatientDB.DeletePatient(tempID);
App.PatientDB.AddNewPatient (patUpdate, tempPin);
DisplayAlert ("Updated", "Your information has been updated!", "OK");
}
VitalsViewModal:
public class VitalsViewModel: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public VitalsViewModel (Patient patient)
{
vitals = patient.Vitals;
}
private List<Vitals> _vitals;
public List<Vitals> vitals {
get {return _vitals; }
set {
if (_vitals != value) {
_vitals = value;
OnPropertyChanged ("vitals");
}
}
}
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this,
new PropertyChangedEventArgs(propertyName));
}
}
}
VitalsView
public partial class VitalsView : ContentPage, INotifyPropertyChanged
{
PatientManager patientManager = new PatientManager ();
Patient globalPatient;
public event PropertyChangedEventHandler PropertyChanged;
public VitalsView (Patient patientZero)
{
InitializeComponent ();
BindingContext = new VitalsViewModel (patientZero);
}
}
Xaml
<ListView x:Name="Vitals" ItemsSource="{Binding vitals}" RowHeight="80" BackgroundColor="Transparent">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ViewCell.View>
<StackLayout Orientation="Vertical" Spacing="0" Padding="15">
<Grid>
<Label Font="17" Text="{Binding Height} " FontAttributes="Bold" TextColor="#449BC4" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="3" />
<Label Font="14" Text="{Binding Weight, StringFormat='Weight: {0}'}" FontAttributes="Bold" Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="3" />
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
</Grid>
</StackLayout>
</ViewCell.View>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
For vitals to have a change in Xaml, something must replace the whole list, of List<vitals with a new list.
Even though the patient changed and its vitals are new from the update, you have bound to an orphaned patient.vitals whose patient reference is still valid. Hence no change.
You need to specifically change the reference of vitals away from the old one to the new one.
I suggest this:
async void OnUpdate(object sender, EventArgs e)
{
string tempUser = globalPatient.Username;
string tempPin = globalPatient.PIN;
patUpdate = patientManager.GetPatientByUsername (tempUser, tempPin).Result;
App.PatientDB.DeletePatient(tempID);
App.PatientDB.AddNewPatient (patUpdate, tempPin);
MyCurrenViewModel.vitals = patUpdate.Vitals; // Replace old vitals
DisplayAlert ("Updated", "Your information has been updated!", "OK");
}
Note In the above example I would create a property named MyCurrentViewModel on the page, and when assigning the datacontext I would have
public partial class VitalsView : ContentPage, INotifyPropertyChanged
{
VitalsViewModel MyCurrentViewModel { get; set; }
PatientManager patientManager = new PatientManager ();
PatientDemo globalPatient;
public event PropertyChangedEventHandler PropertyChanged;
public VitalsView (Patient patientZero)
{
InitializeComponent ();
//BindingContext = new VitalsViewModel (patientZero);
BindingContext = MyCurrentViewModel = new VitalsViewModel (patientZero);
}
}
Code Review Other Errors
OnUpdate is async which is great, but it never awaits any method call; hence making all calls to it synchronous in nature and blocking the gui thread waiting on results. Never block a gui thread, the app will appear to freeze.
As an option, you can use ObservableCollection instead of List.