Xamarin Bindable property not binding - c#

I have been trying to make this small demo app, that is basically a timer with storable time intervals. I dislike the timepicker controls that are used in the android version so I decided to make my own custom control (using ContentView instead of a custom renderer) to group three pickers for hours, minutes and seconds and update a single TimeSpan object.
My goal is to have a ContentView that is used to represent TimeSpan as parts of a ListView cell, here is how it looks for a better understanding. Problem is I can't seem to get the binding to work proprely. As shown in the image all values are zero, though in this mock option the should all have minute values set. Instead they are all set to zero, and as much as I check they also don't set back all the way back to the model.
As it's supposed to work is when the user selects a value in on of the fields on the right --> it should set the boud property string in the view model --> which should update the value of the TimeSpan property --> which should update the model. And the other way should look like: the list view item gets rendered --> the value of the TimeSpan property is bound --> this sets the value in the viewmodel --> this updates the model.
I'll admit I'm a bit fuzzy on how exactly the end value in the ViewModel is actually bound to the custom control (as the other way would seem to need to work when calling propertyChanged callback).
TimeSpanPropertyChanged and the TimeSpan accessors don't seem to be called after the initial values are set (break point doesn't trigger), unless I use a static value (like "1" instead of "(Binding Duration)" which is the property name in the model). The data returned from the mock service is correct up to and including the creation of PresetViewModel objects.
PresetsPage.xaml
<ListView ItemsSource="{Binding Intervals}" SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate >
<ViewCell>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Entry Text="{Binding Name}"/>
<controls:CustomTimeOffsetSelector TimeSpan="{Binding Duration}" Grid.Column="1"/>
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.Footer>
<Button Text="Add interval"/>
</ListView.Footer>
</ListView>
It's values are bound to the view model
PresetViewModel.cs
public class PresetViewModel : INotifyPropertyChanged
{
private ObservableCollection<Interval> intervals;
public event PropertyChangedEventHandler PropertyChanged = (o, e) => { };
public Preset Data { get; private set; }
public string Name
{
get => Data.Name;
set
{
Data.Name = value;
PropertyChanged(this, new PropertyChangedEventArgs("Name"));
}
}
public ObservableCollection<Interval> Intervals
{
get => intervals;
set
{
intervals = value;
PropertyChanged(this, new PropertyChangedEventArgs("Intervals"));
}
}
public PresetViewModel(Preset preset)
{
Data = preset;
Intervals = new ObservableCollection<Interval>(Data.Intervals);
}
//...
}
The Custom control looks like:
CustomTimeOffsetSelector.xaml
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="App2.Controls.CustomTimeOffsetSelector">
<ContentView.Content>
<StackLayout Orientation="Horizontal">
<Picker ItemsSource="{Binding HourList}" SelectedItem="{Binding Hours}"/>
<Label Text=":" FontSize="Small" VerticalTextAlignment="Center" Margin="0"/>
<Picker ItemsSource="{Binding MinuteList}" SelectedItem="{Binding Minutes}"/>
<Label Text=":" FontSize="Small" VerticalTextAlignment="Center" Margin="0"/>
<Picker ItemsSource="{Binding SecondList}" SelectedItem="{Binding Seconds}"/>
</StackLayout>
</ContentView.Content>
</ContentView>
CustomTimeOffsetSelector.xaml.cs
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class CustomTimeOffsetSelector : ContentView
{
public static readonly BindableProperty TimeSpanProperty =
BindableProperty.Create(
nameof(TimeSpan),
typeof(TimeSpan),
typeof(CustomTimeOffsetSelector),
defaultValue: TimeSpan.Zero,
defaultBindingMode: BindingMode.OneWay,
propertyChanged: TimeSpanPropertyChanged
);
private static void TimeSpanPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = bindable as CustomTimeOffsetSelector;
control.viewModel.Data = (TimeSpan)newValue;
}
public TimeSpan TimeSpan
{
get => (TimeSpan)GetValue(TimeSpanProperty);
set => SetValue(TimeSpanProperty, value);
}
IntervalViewModel viewModel;
public CustomTimeOffsetSelector()
{
viewModel = new IntervalViewModel();
BindingContext = viewModel;
InitializeComponent();
}
}
And finaly the view model.
IntervalViewModel.cs
public class IntervalViewModel : INotifyPropertyChanged
{
#region List init area
private static List<string> hourValues;
public static List<string> HourValues
{
get
{
if (hourValues == null)
{
hourValues = new List<string>();
for (int i = 0; i < 24; i++)
hourValues.Add(i.ToString("00"));
}
return hourValues;
}
}
private static List<string> minuteValues;
public static List<string> MinuteValues
{
get
{
if (minuteValues == null)
{
minuteValues = new List<string>();
for (int i = 0; i < 60; i++)
minuteValues.Add(i.ToString("00"));
}
return minuteValues;
}
}
private static List<string> secondValues;
public static List<string> SecondValues
{
get
{
if (secondValues == null)
{
secondValues = new List<string>();
for (int i = 0; i < 60; i++)
secondValues.Add(i.ToString("00"));
}
return secondValues;
}
}
public List<string> HourList => HourValues;
public List<string> MinuteList => MinuteValues;
public List<string> SecondList => SecondValues;
#endregion
private TimeSpan data;
public TimeSpan Data
{
get => data;
set
{
data = value;
PropertyChanged(this, new PropertyChangedEventArgs("Hours"));
PropertyChanged(this, new PropertyChangedEventArgs("Minutes"));
PropertyChanged(this, new PropertyChangedEventArgs("Seconds"));
}
}
public string Hours
{
get
{
return data.Hours.ToString("00");
}
set
{
data = new TimeSpan(int.Parse(value), data.Minutes, data.Seconds);
PropertyChanged(this, new PropertyChangedEventArgs("Hours"));
}
}
public string Minutes
{
get
{
return data.Minutes.ToString("00");
}
set
{
data = new TimeSpan(data.Hours, int.Parse(value), data.Seconds);
PropertyChanged(this, new PropertyChangedEventArgs("Minutes"));
}
}
public string Seconds
{
get
{
return data.Seconds.ToString("00");
}
set
{
data = new TimeSpan(data.Hours, data.Minutes, int.Parse(value));
PropertyChanged(this, new PropertyChangedEventArgs("Seconds"));
}
}
public IntervalViewModel()
{
}
public event PropertyChangedEventHandler PropertyChanged;
}
Interval.cs
public class Interval
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; }
public TimeSpan Duration { get; set; }
}
I know some of the names of files and properties are not the best ones, as well as that using a base ViewModel class would make evrything cleaner my intention was to refactor after I get it working.
I've just been staring at this issue for the last two days and can't seem to think of anything and the answers I find seem to give similar code to what I have currently, which so far has not worked. I'd really appreciate any help in the matter - thank you.

Related

WPF DataGrid calling command, binding

I have just started learning WPF yesterday and my goal is to create window with simple grid with hotel booking information. For now there are just room number, number of guests, dates and "Action" columns. In the "Actions" column there is "Save" button. It should be able to save updates or create new booking when clicked in new row. The problem is when I click "Save" button SaveBooking method is not invoked. I'm also not sure how to properly bind to CurrentBooking object. As I am new to WPF I tried to figure it out from few tutorials. Here's what I've created.
XAML:
<Window x:Class="HotelApp.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:HotelApp"
mc:Ignorable="d"
Title="MainWindow" Height="800" Width="1000">
<Grid>
<TabControl>
<TabItem Header="Bookings">
<DataGrid AutoGenerateColumns = "False" ItemsSource="{Binding Bookings}">
<DataGrid.Columns>
<DataGridTextColumn Header = "Room" Binding = "{Binding Room, Mode=TwoWay}" />
<DataGridTextColumn Header = "Floor" Binding = "{Binding NumOfGuests, Mode=TwoWay}" />
<DataGridTextColumn Header = "From" Binding = "{Binding From, Mode=TwoWay}"/>
<DataGridTextColumn Header = "To" Binding = "{Binding To, Mode=TwoWay}"/>
<DataGridTemplateColumn Header = "Actions">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="Save" Command="{Binding DataContext.SaveBookingCommand }" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</TabItem>
<TabItem Header="Guests" />
</TabControl>
</Grid>
</Window>
MODEL:
public class BookingModel : ObservableObject
{
private int _room;
public int Room
{
get => _room;
set
{
if (value != _room)
{
_room = value;
OnPropertyChanged("Room");
}
}
}
private int _numOfGuests;
public int NumOfGuests
{
get => _numOfGuests;
set
{
_numOfGuests = value;
OnPropertyChanged("NumOfGuests");
}
}
private DateTime _from;
public DateTime From
{
get => _from;
set
{
_from = value;
OnPropertyChanged("From");
}
}
private DateTime _to;
public DateTime To
{
get => _to;
set
{
_to = value;
OnPropertyChanged("To");
}
}
}
VIEWMODEL:
public class MainWindowVM : ObservableObject
{
private readonly IBookingService _bookingService;
private ICommand _saveBookingCommand;
public ICommand SaveBookingCommand
{
get
{
if (_saveBookingCommand == null)
{
_saveBookingCommand = new RelayCommand(
param => SaveBooking(),
param => (CurrentBooking != null)
);
}
return _saveBookingCommand;
}
}
private ObservableCollection<BookingModel> _Bookings { get; set; }
private BookingModel _currentBookng;
public BookingModel CurrentBooking
{
get { return _currentBookng; }
set
{
if (value != _currentBookng)
{
_currentBookng = value;
OnPropertyChanged("CurrentBooking");
}
}
}
public ObservableCollection<BookingModel> Bookings
{
get { return _Bookings; }
set { _Bookings = value; }
}
public MainWindowVM(IBookingService bookingService)
{
_bookingService = bookingService;
BrowseBookings();
}
public void BrowseBookings()
{
var bookings = _bookingService.Browse().Select(x => new BookingModel { Room = x.Room.RoomId, NumOfGuests = x.NumOfGuests, From = x.From, To = x.To });
Bookings = new ObservableCollection<BookingModel>(bookings);
}
private void SaveBooking()
{
// send CurrentBooking to service
}
}
RelayCommand:
public class RelayCommand : ICommand
{
#region Fields
readonly Action<object> _execute;
readonly Predicate<object> _canExecute;
#endregion // Fields
#region Constructors
public RelayCommand(Action<object> execute)
: this(execute, null)
{
}
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
if (execute == null)
throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute;
}
#endregion // Constructors
#region ICommand Members
[DebuggerStepThrough]
public bool CanExecute(object parameters)
{
return _canExecute == null ? true : _canExecute(parameters);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameters)
{
_execute(parameters);
}
#endregion // ICommand Members
}
Your command is in the datacontext of the entire datagrid MainWindowVM.
Your button's datacontext is that of the row - a BookingModel.
You need some relativesource on that binding.
In principle that looks like this:
{Binding DataContext.ParentVMProperty,
RelativeSource={RelativeSource AncestorType={x:Type typeOfAncestor}}}
And your type, in this case, will be DataGrid.
You can also bind selecteditem on the datagrid and when they click the button ensure that is selected using the datagrid properties for selection.
Or
You can have a commandparameter on the command which is
CommandParameter="{Binding .}"
Relaycommand usually comes in two flavours one being RelayCommand
Maybe I missed it but I don't see that in your implementation. I'd suggest you go grab the source code for MVVM Light and paste into your solution for a more complete implementation. Or just add the nuget package if you're not using .net core. You want the commandwpf namespace version of relaycommand.
You left out a lot of code, so I don't know which nuget package you used for your ObservableObject. Anywho, I faked the ObservableObject and got the binding working. The main problem was that you were trying to bind SaveBookingCommand at the BookingModel level, when in your code you have it written in the MainWindowVM level.
You can easily fix this by parenting your MainWindowVM in your BookingModel, and change your binding to be Command={Binding Parent.SaveBookingCommand}.
Here's some pointers to the edits that I made:
MainWindow.xaml.cs:
<DataTemplate>
<Button Content="Save" Command="{Binding Parent.SaveBookingCommand}" />
</DataTemplate>
BookingModel.cs:
public class BookingModel : ObservableObject
{
public MainWindowVM Parent { get; private set; }
public BookingModel()
{
this.Parent = null;
}
public BookingModel(MainWindowVM parent)
{
this.Parent = parent;
}
// ... you know the rest
MainWindowVM.cs:
public MainWindowVM : ObservableObject
{
public void BrowseBookings()
{
// NOTICE that I added 'this' as the parameter argument to connect MainWindowVM to the BookingModel.
var bookings = _bookingService.Browse().Select(x => new BookingModel(this) { Room = x.Room, NumOfGuests = x.NumOfGuests, From = x.From, To = x.To });
Bookings = new ObservableCollection<BookingModel>(bookings);
CurrentBooking = Bookings.First();
}
// ... you know the rest

ListView ItemsSource binding not displaying items

I am trying to create a bound ListView in Xamarin. Here's the C# code:
public partial class LearnPage : ContentPage
{
public class TodoItem
{
public string DisplayName { get; set; }
}
//ObservableCollection<TodoItem> Items = new ObservableCollection<TodoItem>();
private IEnumerable<TodoItem> _items;
public IEnumerable<TodoItem> Items
{
get { return _items; }
set
{
if (Equals(_items, value))
return;
_items = value;
OnPropertyChanged();
}
}
public LearnPage ()
{
InitializeComponent();
BindingContext = this;
Items = new TodoItem[]{
new TodoItem{ DisplayName = "Milk cartons are recyclable" }
};
//Items.Add(new TodoItem { DisplayName = "Milk cartons are recyclable" });
}
}
You can also see some commented out code with an ObervableCollection, which I have also tried with.
And here's the XAML:
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="learn.LearnPage">
<ContentPage.Content>
<StackLayout VerticalOptions="FillAndExpand" Padding="0,10,0,10">
<ListView ItemsSource="{Binding Items}" RowHeight="40" x:Name="sarasas">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding DisplayName}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage.Content>
When I build the app, an empty list is displayed. I'm really new to Xamarin and I think I'm just missing something obvious. Any help appreciated!
I am not sure if ContentPage uses [CallerMemberName] for the OnPropertyChanged() method. So first thing I would try is to write OnPropertyChanged("Items") instead.
Either way, if I were you I would separate concerns and move the Items into a ViewModel class, which implements INotifyPropertyChanged itself. This will help later on if you want to test, add more code such as commands, inject dependencies etc., where you will keep ViewModel code separate from View code.
So you could start with:
public abstract class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged ([CallerMemberName]string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs (propertyName));
}
}
Then create your ViewModel:
public class LearnViewModel : BaseViewModel
{
public ObservableCollection<TodoItem> Items { get; } = new ObservableCollection<TodoItem>();
private ICommand _addTodoItem;
public ICommand AddTodoItem =>
_addTodoItem = _addTodoItem ?? new Command(DoAddTodoItem);
private int _count;
private void DoAddTodoItem()
{
var item = new TodoItem { DisplayName = $"Item {++_count}" };
// NotifyCollectionChanged must happen on UI thread.
Device.BeginInvokeOnMainThread (() => {
Items.Add(item);
});
}
}
Then you can keep your View code thin like:
public partial class LearnPage : ContentPage
{
public LearnPage ()
{
InitializeComponent();
BindingContext = new LearnViewModel();
}
}
Normally you would invoke a command to populate the Items in your ViewModel, this could be by fetching data from the Internet, or loading local data from a database etc.
In this sample you can just add a constructor to the LearnViewModel and call DoAddTodoItem a couple of times:
public class LearnViewModel : BaseViewModel
{
public LearnViewModel()
{
DoAddTodoItem();
DoAddTodoItem();
}
...
This should show you something like this when you launch the app:

WPF nested properties from selected item in databound listBox

I've tried solving this myself, looking at several possible solutions here on Stack Overflow, but alas I've been unable to solve this issue.
TL;DR version:
The problem:
A listBox using databinding to show a list of RPG characters, which have a nested property for their attributes. I can't get the attributes to show due to the limitations with nested properties and databindings.
The code below is related to the issue.
I have a listBox that has a databinding that controls what is shown in the list. The databinding uses ObservableCollection for the list of objects that the list contains. All this works fine, but is related to the issue at hand.
The listBox databinding has several nested properties in each element, that I want to display and change in the form, yet I cannot get nested databinding to work correctly.
This is the listBox XAML:
<ListBox x:Name="listCharacters" Margin="2,0" ItemsSource="{Binding}" Grid.Row="1" ScrollViewer.VerticalScrollBarVisibility="Visible" ScrollViewer.HorizontalScrollBarVisibility="Hidden" SelectionChanged="listCharacters_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type local:Character}" x:Name="Symchar">
<Grid Width="125" HorizontalAlignment="Left" Background="{x:Null}">
<Grid.RowDefinitions>
<RowDefinition Height="18"/>
<RowDefinition Height="12"/>
<RowDefinition Height="16"/>
</Grid.RowDefinitions>
<Image Panel.ZIndex="5" HorizontalAlignment="Right" VerticalAlignment="Top" Height="16" Width="16" Margin="0,2,0,0" Source="Resources/1454889983_cross.png" MouseUp="DeleteCharacter" />
<TextBlock Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" FontWeight="Bold" FontSize="13.333" Grid.Row="0" TextTrimming="CharacterEllipsis" Padding="0,0,16,0" />
<TextBlock Text="{Binding RealRace.Label, UpdateSourceTrigger=PropertyChanged}" FontSize="9.333" Grid.Row="1" FontStyle="Italic" />
<TextBlock FontSize="9.333" Grid.Row="2" HorizontalAlignment="Left" VerticalAlignment="Top">
<Run Text="{Binding RealClass.Archetype.Label, UpdateSourceTrigger=PropertyChanged}"/>
<Run Text=" - "/>
<Run Text="{Binding RealClass.Label, UpdateSourceTrigger=PropertyChanged}"/>
</TextBlock>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
And setting the listBox ItemSource:
this.listCharacters.ItemsSource = CharacterList;
This is the character class, I removed unrelated code (XML serialization attributes etc.)
public class Character : INotifyPropertyChanged
{
private string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
this.NotifyPropertyChanged("Name");
}
}
private string _player;
public string Player
{
get { return _player; }
set
{
_player = value;
this.NotifyPropertyChanged("Player");
}
}
private string _race;
public string Race
{
get { return _race; }
set
{
_race = value;
this.NotifyPropertyChanged("Race");
}
}
private Race _realRace;
public Race RealRace
{
get { return _realRace; }
set
{
_realRace = value;
Race = value.Id;
this.NotifyPropertyChanged("RealRace");
}
}
private string _gender;
public string Gender
{
get { return _gender; }
set
{
_gender = value;
this.NotifyPropertyChanged("Gender");
}
}
private Attributes _attributes;
public Attributes Attributes
{
get { return _attributes; }
set
{
_attributes = value;
this.NotifyPropertyChanged("Attributes");
}
}
private string _class;
public string Class
{
get { return _class; }
set
{
_class = value;
this.NotifyPropertyChanged("Class");
}
}
private Class _realClass;
public Class RealClass
{
get { return _realClass; }
set
{
_realClass = value;
Class = value.Id;
this.NotifyPropertyChanged("RealClass");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
To keep it simple, the property that I've been testing with, is the 'Attributes' property, this is the code for it:
public class Attributes : INotifyPropertyChanged
{
private int _accurate;
public int Accurate
{
get { return _accurate; }
set
{
_accurate = value;
this.NotifyPropertyChanged("Accurate");
}
}
private int _cunning;
public int Cunning
{
get { return _cunning; }
set
{
_cunning = value;
this.NotifyPropertyChanged("Cunning");
}
}
private int _discreet;
public int Discreet
{
get { return _discreet; }
set
{
_discreet = value;
this.NotifyPropertyChanged("Discreet");
}
}
private int _persuasive;
public int Persuasive
{
get { return _persuasive; }
set
{
_persuasive = value;
this.NotifyPropertyChanged("Persuasive");
}
}
private int _quick;
public int Quick
{
get { return _quick; }
set
{
_quick = value;
this.NotifyPropertyChanged("Quick");
}
}
private int _resolute;
public int Resolute
{
get { return _resolute; }
set
{
_resolute = value;
this.NotifyPropertyChanged("Resolute");
}
}
private int _strong;
public int Strong
{
get { return _strong; }
set
{
_strong = value;
this.NotifyPropertyChanged("Strong");
}
}
private int _vigilant;
public int Vigilant
{
get { return _vigilant; }
set
{
_vigilant = value;
this.NotifyPropertyChanged("Vigilant");
}
}
private int _toughness;
public int Toughness
{
get { return _toughness; }
set
{
_toughness = value;
this.NotifyPropertyChanged("Toughness");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
I want to display each individual attribute in a field when a character in the listBox is selected, this works fine with properties directly in the character class, but due to the limitations on nested properties and databindings, I haven't been able to get it to work with the 'Attributes' properties values.
XAML for one of the attribute input fields:
<TextBox x:Name="attr_Accurate" DataContext="{Binding Path=(local:Character.Attributes), XPath=SelectedItem, ElementName=listCharacters, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Text="{Binding Path=(local:Accurate)}" PreviewTextInput="NumericInput"/>
The UpdateSourceTrigger is simply a method to only allow integers to be input in the field:
private void NumericInput(object sender, TextCompositionEventArgs e)
{
if (!char.IsDigit(e.Text, e.Text.Length - 1))
{
e.Handled = true;
}
}
If anyone could help me get the values within the selected character's attributes to show up via databindings, I would greatly appreciate it.
Use binding as following:
To Show Accurate Property value:
<TextBox x:Name="attr_Accurate" Text="{Binding Path=SelectedItem.Attributes.Accurate), ElementName=listCharacters, Mode=OneWay}" PreviewTextInput="NumericInput"/>
To Show Cunning property value:
<TextBox x:Name="attr_Accurate" Text="{Binding Path=SelectedItem.Attributes.Cunning), ElementName=listCharacters, Mode=OneWay}" PreviewTextInput="NumericInput"/>
and so one.
(I'm not sure if you want binding to be two way or one so please
change as your need)

Databinding not working with ViewModel

Cant get any data to work with databinding, I have the INotify event, I have the binding on the xaml objects, but nothing shows up, if I change the content on the lables to "something" it works, but nothing shows on load or on click on my button
My Xaml view
<Grid>
<StackPanel Name="stackpanel">
<Label Content="{Binding Name}" />
<Label Content="{Binding Length}" />
<Label Content="{Binding Rating}" />
<Button Content="Change text" Click="ButtonClick" />
</StackPanel>
</Grid>
Its codebehind
public partial class Movie
{
readonly MovieViewModel _movieViewModel;
public Movie()
{
InitializeComponent();
_movieViewModel = new MovieViewModel { Movie = { Name = "The Dark Knight", Length = 180, Rating = 88 } };
stackpanel.DataContext = _movieViewModel;
}
private void ButtonClick(object sender, RoutedEventArgs e)
{
_movieViewModel.Movie.Name = "bad movie";
}
}
The View Model
class MovieViewModel
{
public MovieViewModel() : this(new Movie())
{
}
public MovieViewModel(Movie movie)
{
Movie = movie;
}
public Movie Movie { get; set; }
}
The Model
class Movie : INotifyPropertyChanged
{
public Movie()
{}
private string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
NotifyPropertyChanged("Name");
}
}
private int _length;
public int Length
{
get { return _length; }
set
{
_length = value;
NotifyPropertyChanged("Length");
}
}
private int _rating;
public int Rating
{
get { return _rating; }
set
{
if (_rating == value) return;
_rating = value;
NotifyPropertyChanged("_Rating");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
You have your bindings set incorrectly, that's the reason nothing is being shown.
Just take a closer look at your ViewModel and than on the bindings. You try to bind to property named Name but your MovieViewModel does not expose any property with that name. I'm pretty sure binding errors were reported to you (look through messages in Output window).
To make it work, you need either expose properties in your ViewModel to match the ones you try to bind to (bad), or change bindings in your xaml to have correct path:
<Label Content="{Binding Movie.Name}" />
<Label Content="{Binding Movie.Length}" />
<Label Content="{Binding Movie.Rating}" />
This should get you going.
Additionally - you may want to implement INotifyPropertyChanged also on your MovieViewModel class if you plan to change Movie object that is assigned to Movie property. As long as you will only change properties of Movie object already assigned to MovieViewModel everything will be ok, but if you would try to change actual object assigned to this property, no changes notifications will be generated and your UI will stop working correctly.
Moreover - I noticed that you made your NotifyPorpertyChanged method public - I wouldn't advise this as anyone can now trigger this event. Normal approach is to make such methods private or protected, depending if you want to provide way to trigger event from inheriting classes (which is very likely in case of PropertyChanged event).
I think you have one typing mistake
NotifyPropertyChanged("_Rating");
Should be
NotifyPropertyChanged("Rating");
Rather than using Label, I would suggest you to use Texblock. Try the following code
_movieViewModel = new MovieViewModel
{ Movie = { Name = "The Dark Knight", Length = 180, Rating = 88 } };
this.DataContext = _movieViewModel;
and
Textblock like following
<StackPanel Name="stackpanel">
<TextBlock Name="textBlock1" Text="{Binding Path=Name}"/>
<TextBlock Name="textBlock2" Text="{Binding Path=Length}"/>
<Button Content="Change text" Click="ButtonClick" />
</StackPanel>

ObservableCollection not updating View

I am just starting with MVVM and have hit a hurdle that I hope someone can help me with. I am trying to create a simple View with 2 listboxes. A selection from the first listbox will populate the second list box. I have a class created that stores the information I want to bind to.
MyObject Class (Observable Object is just a base class that implements INotifyPopertyChanged)
public class MyObject : ObservableObject
{
String _name = String.Empty;
ObservableCollection<MyObject> _subcategories;
public ObservableCollection<MyObject> SubCategories
{
get { return _subcategories; }
set
{
_subcategories = value;
RaisePropertyChanged("SubCategories");
}
}
public String Name
{
get { return _name; }
set
{
_name = value;
RaisePropertyChanged("Name");
}
}
public MyObject()
{
_subcategories = new ObservableCollection<EMSMenuItem>();
}
}
In my viewmodel I have two ObservableCollections created
public ObservableCollection<EMSMenuItem> Level1MenuItems { get; set; }
public ObservableCollection<EMSMenuItem> Level2MenuItems { get; set; }
In my constructor of the ViewModel I have:
this.Level1MenuItems = new ObservableCollection<EMSMenuItem>();
this.Level2MenuItems = new ObservableCollection<EMSMenuItem>();
this.Level1MenuItems = LoadEMSMenuItems("Sample.Xml");
That works fine for the Level1 items and they correctly show in the View. However I have a command that gets called when the user clicks an item in the listbox, which has the following:
Level2MenuItems = ClickedItem.SubCategories;
For some reason this does not update the UI of the second listbox. If I put a breakpoint at this location I can see that Level2MenuItems has the correct information stored in it. If I write a foreach loop and add them individually to the Level2MenuItems collection then it does display correctly.
Also as a test I added the following to the constructor:
Level2MenuItems = Level1MenuItems[0].SubCategories;
And that updated correctly.
So why would the code work as expected in the constructor, or when looping through, but not when a user clicks on an item in the listbox?
You need to raise the change notification on the Level2MenuItems property.
Instead of having
public ObservableCollection<EMSMenuItem> Level2MenuItems { get; set; }
you need
private ObservableCollection<EMSMenuItem> _level2MenuItems;
public ObservableCollection<EMSMenuItem> Level2MenuItems
{
get { return _level2MenuItems; }
set
{
_level2MenuItems = value;
RaisePropertyChanged(nameof(Level2MenuItems));
}
}
The reason the former works in the constructor is that the Binding has not taken place yet. However since you are changing the reference via a command execute which happens after the binding you need to tell view that it changed
You need to make your poco class within the ObservableCollection implement INotifyPropertyChanged.
Example:
<viewModels:LocationsViewModel x:Key="viewModel" />
.
.
.
<ListView
DataContext="{StaticResource viewModel}"
ItemsSource="{Binding Locations}"
IsItemClickEnabled="True"
ItemClick="GroupSection_ItemClick"
ContinuumNavigationTransitionInfo.ExitElementContainer="True">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" Margin="0,0,10,0" Style="{ThemeResource ListViewItemTextBlockStyle}" />
<TextBlock Text="{Binding Latitude, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Style="{ThemeResource ListViewItemTextBlockStyle}" Margin="0,0,5,0"/>
<TextBlock Text="{Binding Longitude, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Style="{ThemeResource ListViewItemTextBlockStyle}" Margin="5,0,0,0" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
public class LocationViewModel : BaseViewModel
{
ObservableCollection<Location> _locations = new ObservableCollection<Location>();
public ObservableCollection<Location> Locations
{
get
{
return _locations;
}
set
{
if (_locations != value)
{
_locations = value;
OnNotifyPropertyChanged();
}
}
}
}
public class Location : BaseViewModel
{
int _locationId = 0;
public int LocationId
{
get
{
return _locationId;
}
set
{
if (_locationId != value)
{
_locationId = value;
OnNotifyPropertyChanged();
}
}
}
string _name = null;
public string Name
{
get
{
return _name;
}
set
{
if (_name != value)
{
_name = value;
OnNotifyPropertyChanged();
}
}
}
float _latitude = 0;
public float Latitude
{
get
{
return _latitude;
}
set
{
if (_latitude != value)
{
_latitude = value;
OnNotifyPropertyChanged();
}
}
}
float _longitude = 0;
public float Longitude
{
get
{
return _longitude;
}
set
{
if (_longitude != value)
{
_longitude = value;
OnNotifyPropertyChanged();
}
}
}
}
public class BaseViewModel : INotifyPropertyChanged
{
#region Events
public event PropertyChangedEventHandler PropertyChanged;
#endregion
protected void OnNotifyPropertyChanged([CallerMemberName] string memberName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(memberName));
}
}
}
Your Subcategories property should be read-only.

Categories

Resources