I was hoping to get some help with a custom DateTimePicker control in .NET MAUI. I am fairly new to MAUI and development in general so I apologize in advance if my question is not clear.
What I am trying to accomplish is the following:
The user selects the entry and the date picker and time picker controls appear.
After the date and time is selected the pickers disappear and the entry label is populated with the selected date time.
I was trying to use the unfocused event on the pickers to hide them but when you select the time picker (as an example) it disappears before the time is fully selected and the label does not update accordingly. Below is what I have so far, any help or suggestions are greatly appreciated!
XAML:
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:inputLayout="clr-namespace:Syncfusion.Maui.Core;assembly=Syncfusion.Maui.Core"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
x:Class="TestApplication.View.DateTimePicker">
<VerticalStackLayout Padding="10">
<inputLayout:SfTextInputLayout x:Name="EntryField"
IsHintAlwaysFloated="True"
ContainerType="Outlined"
ContainerBackground="Transparent">
<inputLayout:SfTextInputLayout.GestureRecognizers>
<TapGestureRecognizer Tapped="Entry_Tapped" />
</inputLayout:SfTextInputLayout.GestureRecognizers>
<Entry IsReadOnly="True" x:Name="entry"/>
<inputLayout:SfTextInputLayout.LeadingView>
<ImageButton Source="calendar.png"/>
</inputLayout:SfTextInputLayout.LeadingView>
</inputLayout:SfTextInputLayout>
<HorizontalStackLayout Spacing="10" IsVisible="False" x:Name="picker" HorizontalOptions="Center">
<DatePicker x:Name="datePicker" DateSelected="DatePicker_DateSelected" Format="MM/dd/yyyy" Unfocused="Picker_Unfocused"/>
<TimePicker x:Name="timePicker" Focused="TimePicker_TimeSelected" Unfocused="Picker_Unfocused"/>
</HorizontalStackLayout>
</VerticalStackLayout>
</ContentView>
Code behind:
using System.ComponentModel;
namespace TestApplication.View;
public partial class DateTimePicker : ContentView, INotifyPropertyChanged
{
public DateTimePicker()
{
InitializeComponent();
}
public static readonly BindableProperty DateTimeProperty =
BindableProperty.Create(nameof(DateTime), typeof(DateTime), typeof(DateTimePicker), DateTime.Now, BindingMode.TwoWay);
public event EventHandler DateTimeUpdated;
public event PropertyChangedEventHandler PropertyChanged;
public DateTime DateTime
{
get => (DateTime)GetValue(DateTimeProperty);
set
{
SetValue(DateTimeProperty, value);
OnPropertyChanged("DateTime");
DateTimeUpdated?.Invoke(this, EventArgs.Empty);
}
}
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
DateTimeUpdated?.Invoke(this, EventArgs.Empty);
}
private void DatePicker_DateSelected(object sender, DateChangedEventArgs e)
{
DateTime = new DateTime(e.NewDate.Year, e.NewDate.Month, e.NewDate.Day, timePicker.Time.Hours, timePicker.Time.Minutes, 0);
entry.Text = DateTime.ToString("MM/dd/yyyy h:mm tt");
DateTimeUpdated?.Invoke(this, EventArgs.Empty);
}
private void TimePicker_TimeSelected(object sender, FocusEventArgs e)
{
DateTime = new DateTime(DateTime.Year, DateTime.Month, DateTime.Day, timePicker.Time.Hours, timePicker.Time.Minutes, 0);
entry.Text = DateTime.ToString("MM/dd/yyyy h:mm tt");
DateTimeUpdated?.Invoke(this, EventArgs.Empty);
}
private void Entry_Tapped(object sender, EventArgs e)
{
picker.IsVisible = !picker.IsVisible;
}
private void Picker_Unfocused(object sender, FocusEventArgs e)
{
picker.IsVisible = false;
}
}
You can try to use MVVM to achieve:
public class MyVM : INotifyPropertyChanged
{
private DateTime _selectedDate;
private TimeSpan _selectedTime;
public MyVM()
{
_selectedDate = DateTime.Today;
_selectedTime = DateTime.Now.TimeOfDay;
}
public DateTime SelectedDate
{
get { return _selectedDate; }
set {
if (_selectedDate != value)
{
_selectedDate = value;
OnPropertyChanged(nameof(SelectedDate));
OnPropertyChanged(nameof(SelectedDateTime));
OnPropertyChanged(nameof(SelectedDateTimeString));
}
}
}
public TimeSpan SelectedTime
{
get { return _selectedTime; }
set
{
if (_selectedTime != value)
{
_selectedTime = value;
OnPropertyChanged(nameof(SelectedTime));
OnPropertyChanged(nameof(SelectedDateTime));
OnPropertyChanged(nameof(SelectedDateTimeString));
}
}
}
public DateTime SelectedDateTime
{
get { return new DateTime(SelectedDate.Year, SelectedDate.Month, SelectedDate.Day, SelectedTime.Hours, SelectedTime.Minutes, SelectedTime.Seconds); }
}
public string SelectedDateTimeString
{
get { return SelectedDateTime.ToString("MM/dd/yyyy hh:mm tt"); }
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Xaml:
<StackLayout>
<Entry x:Name="entry"
Text="{Binding SelectedDateTimeString}"
Focused="OnEntryFocused"/>
<Grid x:Name="pickerLayout" IsVisible="false">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<DatePicker Grid.Row="0" Date="{Binding SelectedDate}" />
<TimePicker Grid.Row="1" Time="{Binding SelectedTime}" />
<Button Grid.Row="2" Text="OK" Clicked="OnPickerOKClicked"/>
</Grid>
</StackLayout>
Xamal.cs file:
public partial class MyPicker : ContentView
{
MyVM vm = new MyVM();
public MyPicker()
{
InitializeComponent();
BindingContext = vm;
}
private async void OnEntryFocused(object sender, FocusEventArgs e)
{
entry.IsVisible = false;
pickerLayout.IsVisible = true;
await Task.Run(() => WaitForChangedResult());
}
private async void OnPickerOKClicked(object sender,EventArgs e)
{
pickerLayout.IsVisible = false;
entry.IsVisible = true;
OnPropertyChanged(nameof(vm.SelectedDateTimeString));
}
private void WaitForChangedResult() {
while (pickerLayout.IsVisible)
{
Thread.Sleep(100);
}
}
}
Related
I have a Xamarin form where I have a list and two buttons. What I am seeing is that, depending where the buttons are, the model loads differently. Here is my Xaml code:
<?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="MyApp.Views.RewardsPage"
Title="{Binding Title}"
xmlns:local="clr-namespace:MyApp.ViewModels"
xmlns:model="clr-namespace:MyApp.Models" x:DataType="local:RewardsViewModel"
x:Name="BrowseItemsPage">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="650" />
</Grid.RowDefinitions>
<CollectionView x:Name="ItemsListView"
ItemsSource="{Binding Items}"
SelectionMode="None" Grid.Row="1" >
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout Padding="10" x:DataType="model:RewardModel">
<Label Text="{Binding id, StringFormat='ID: {0}'}"
LineBreakMode="NoWrap"
Style="{DynamicResource ListItemTextStyle}"
FontSize="16" />
<!--other labels removed for brevity-->
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
<CollectionView.Footer>
<StackLayout Orientation="Horizontal">
<Button Text="Previous Month" Command="{Binding PreviousMonthCommand}" HorizontalOptions="FillAndExpand"></Button>
<Button Text="Next Month" Command="{Binding NextMonthCommand}" HorizontalOptions="FillAndExpand"></Button>
</StackLayout>
</CollectionView.Footer>
</CollectionView>
</Grid>
</ContentPage>
The code works fine here. But, if I move the StackLayout from the CollectionView.Footer to its own grid row, like this:
</CollectionView>
<!--other labels removed for brevity-->
</CollectionView>
<StackLayout Orientation="Horizontal" Grid.Row="2">
<Button Text="Previous Month" Command="{Binding PreviousMonthCommand}" HorizontalOptions="FillAndExpand"></Button>
<Button Text="Next Month" Command="{Binding NextMonthCommand}" HorizontalOptions="FillAndExpand"></Button>
</StackLayout>
Then the code in my RewardsViewModel executes in a different order. Here is my RewardsViewModel code (simplified):
[QueryProperty(nameof(CurrentMonth), nameof(CurrentMonth))]
[QueryProperty(nameof(CurrentYear), nameof(CurrentYear))]
public class RewardsViewModel: BaseViewModel
{
public ObservableCollection<RewardModel> Items { get; }
private List<MonthModel> months;
private int _current_year;
private int _current_month;
public Command PreviousMonthCommand { get; }
public Command NextMonthCommand { get; }
public RewardsViewModel()
{
Items = new ObservableCollection<RewardModel>();
PreviousMonthCommand = new Command(OnPrevious, ValidatePrevious);
NextMonthCommand = new Command(OnNext, ValidateNext);
}
public int CurrentYear
{
get
{
return _current_year ;
}
set
{
_current_year = value;
LoadItems();
}
}
public int CurrentMonth
{
get
{
return _current_month;
}
set
{
_current_month= value;
LoadItems();
}
}
public void LoadItems()
{
IsBusy = true;
//do stuff
}
private bool ValidatePrevious()
{
//do stuff to validate and return true or false
}
private bool ValidateNext()
{
//do stuff to validate and return true or false
}
private void OnPrevious()
{
//do stuyff
}
private void OnNext()
{
//do stuff
}
}
Depending on where the buttons reside in the Xaml page, the load events change:
When the buttons are within CollectionView, and the page loads, first the constructor loads then the Query Parameter setters load (CurrentMonth set then CurrentYear set)
When the buttons are outside the CollectionView, first the constructor loads then ValidatePrevious method is called and then ValidateNext method is called.
Why does the placement of the buttons in the Xaml file change the order of operations in my ViewModel? And, how do I ensure that the Query Parameter setters are called first, regardless of where the buttons reside?
Edit:
This is the code, from the previous page, that loads this page, passing in the Query Parameters:
async void OnItemSelected(MonthModel item)
{
if (item == null)
return;
await Shell.Current.GoToAsync($"{nameof(RewardsPage)}?{nameof(RewardsViewModel.CurrentYear)}={CurrentYear}&CurrentMonth={SelectedItem.month}");
}
Edit: Adding Base Class:
public class BaseViewModel : INotifyPropertyChanged
{
bool isBusy = false;
public bool IsBusy
{
get { return isBusy; }
set { SetProperty(ref isBusy, value); }
}
string title = string.Empty;
public string Title
{
get { return title; }
set { SetProperty(ref title, value); }
}
protected bool SetProperty<T>(ref T backingStore, T value,
[CallerMemberName] string propertyName = "",
Action onChanged = null)
{
if (EqualityComparer<T>.Default.Equals(backingStore, value))
return false;
backingStore = value;
onChanged?.Invoke();
OnPropertyChanged(propertyName);
return true;
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
var changed = PropertyChanged;
if (changed == null)
return;
changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
In the view, I have a ListView that should be shown a with a data binding with an Observable collection of string, but not shown anything
If instead of listview I put a label and the observable collection turns it into a simple string I see the data
In the Main view:
<ListView Grid.Row="1" Grid.Column="1" ItemsSource="{Binding SerialsPorts}">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding SerialPortName}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
In the Main ViewModel
class BrightnessSerialsPortsViewModel : INotifyPropertyChanged, IBrightnessSerialsPortsViewModel
{
//readonly IPageDialogService pageDialogService;
readonly IBrightnessSerialsPortsManager manager;
public BrightnessSerialsPortsViewModel()
{
//pageDialogService = new PageDialogService();
manager = new BrightnessSerialsPortsManager();
manager.BrightnessInfoUpdated += OnBrightnessInfoUpdated;
manager.DeviceSerialPortsInfoUpdated += OnDeviceSerialsPortsInfoUpdated;
PageAppearingCommand = new Command(OnPageAppearing);
PageDisappearingCommand = new Command(OnPageDisappearing);
}
void OnPageAppearing()
{
//pageDialogService.DisplayAlert("Invoke Command Demo Page", "Appearing event fired.", "OK");
manager.GetBrightness();
manager.GetSerialsPorts();
}
void OnPageDisappearing()
{
//pageDialogService.DisplayAlert("Invoke Command Demo Page", "Disappearing event fired.", "OK");
SerialTest = "";
}
private void OnDeviceSerialsPortsInfoUpdated(object sender, IDeviceSerialsPortsInfoEventArgs e)
{
foreach(string device in e.DeviceSerialsPorts.Devices)
{
ISerialsPortsViewModel serialsPortsViewModel = new SerialsPortsViewModel(device);
SerialsPorts.Add(serialsPortsViewModel);
SerialTest += device + Environment.NewLine;
}
}
private void OnBrightnessInfoUpdated(object sender, IBrightnessInfoEventArgs e)
{
float f = e.DeviceBrightness.Brightness;
decimal dec = new decimal(f);
Brightness = (double) dec;
}
//public ICommand ChangeBrightnessCommand { get; set; }
public ICommand PageAppearingCommand { get; private set; }
public ICommand PageDisappearingCommand { get; private set; }
public ICommand ChangeBrightnessCommand => new RelayCommand(() => ExcecuteChangeBrightnessCommand());
public void ExcecuteChangeBrightnessCommand()
{
}
private ObservableCollection<ISerialsPortsViewModel> serialsPorts = new ObservableCollection<ISerialsPortsViewModel>();
public ObservableCollection<ISerialsPortsViewModel> SerialsPorts { get=> serialsPorts ; set { serialsPorts = value; OnPropertyChanged(nameof(SerialsPorts)); } }
private string serialstest = "";
public string SerialTest { get => serialstest; set {serialstest = value ; OnPropertyChanged(nameof(SerialTest)); } }
private double brightness = 1.0;
public double Brightness { get => brightness; set {brightness = value ; OnPropertyChanged(nameof(Brightness)); } }
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
In the SerialPorts ViewModel:
public SerialsPortsViewModel(string serialPortName)
{
SerialPortName = serialPortName;
}
private string serialPortName;
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public string SerialPortName { get=> serialPortName; set {serialPortName = value ; OnPropertyChanged(nameof(SerialPortName)); } }
What am I doing wrong?
Solved change the View to this:
<ListView Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2" ItemsSource="{Binding SerialsPorts}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell Height="60">
<StackLayout Orientation="Horizontal">
<BoxView BackgroundColor="Blue" WidthRequest="10" Margin="0,0,0,10" />
<StackLayout BackgroundColor="White" Orientation="Vertical" Margin="5,5,10,5">
<Label Text="{Binding SerialPortName}" FontAttributes="Bold" />
</StackLayout>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
I try to rewrite my UWP C# app for Windows10 to Xamarin app using XAML. But Binding (for example here in ListView ItemSource=...) is not working for me and I don´t know why.
Visual Studio tells me, Cannot Resolve Symbol Recording due to unknown Data Context.
Here is my XAML (MainPage.xaml) for testing purpose:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamarinTest;assembly=XamarinTest"
x:Class="XamarinTest.MainPage">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="100" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="100" />
</Grid.ColumnDefinitions>
<ListView x:Name="listView" IsVisible="false" ItemsSource="{Binding Recording}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ViewCell.View>
<StackLayout Orientation="Horizontal">
<Image Source="Accept" WidthRequest="40" HeightRequest="40" />
<StackLayout Orientation="Vertical" HorizontalOptions="StartAndExpand">
<Label Text="TEST" HorizontalOptions="FillAndExpand" />
<Label Text="TEST" />
</StackLayout>
</StackLayout>
</ViewCell.View>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</ContentPage>
Here is C# (MainPage.xaml.cs):
namespace XamarinTest
{
public partial class MainPage : ContentPage
{
public MainPage()
{
this.InitializeComponent();
this.AllTestViewModel = new RecordingViewModel();
this.BindingContext = AllTestViewModel;
}
public RecordingViewModel AllTestViewModel { get; set; }
}
}
And finally ViewModel (RecordingViewModel.cs):
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using XamarinTest.Model;
namespace XamarinTest.ViewModel
{
public class RecordingViewModel : INotifyPropertyChanged
{
public ObservableCollection<Recording> Recordings { get; } = new TrulyObservableCollection<Recording>();
public RecordingViewModel()
{
Recordings.Add(new RecordingTest2()
{
TestName = "Test 1",
TestNote = "Vytvoreni DB",
TestTime = new TimeSpan(0, 0, 0)
});
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public sealed class TrulyObservableCollection<T> : ObservableCollection<T>
where T : INotifyPropertyChanged
{
public TrulyObservableCollection()
{
CollectionChanged += FullObservableCollectionCollectionChanged;
}
public TrulyObservableCollection(IEnumerable<T> pItems) : this()
{
foreach (var item in pItems)
{
this.Add(item);
}
}
private void FullObservableCollectionCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (Object item in e.NewItems)
{
((INotifyPropertyChanged)item).PropertyChanged += ItemPropertyChanged;
}
}
if (e.OldItems != null)
{
foreach (Object item in e.OldItems)
{
((INotifyPropertyChanged)item).PropertyChanged -= ItemPropertyChanged;
}
}
}
private void ItemPropertyChanged(object sender, PropertyChangedEventArgs e)
{
NotifyCollectionChangedEventArgs args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, sender, sender, IndexOf((T)sender));
OnCollectionChanged(args);
}
}
}
Everything (models and viewmodels) are working in native UWP Windows 10 app. Only the Binding and making same view is problem in Xamarin. Could someone please help with binding?
Thx.
EDIT
Recording.cs is here:
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace XamarinTest.Model
{
public abstract class Recording : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public string TestName { get; set; }
private TimeSpan _testTime;
private string _testNote;
private string _actualIco = "Play";
private bool _isActive = false;
private bool _enabled = true;
public double IcoOpacity { get; private set; } = 1.0;
public string ActualIco
{
get => _actualIco;
set
{
if (_actualIco == null) _actualIco = "Admin";
_actualIco = value;
NotifyPropertyChanged("ActualIco");
}
}
public bool IsActive
{
get => _isActive;
set
{
if (_isActive == value) return;
_isActive = value;
IcoOpacity = !value ? 1.0 : 0.3;
NotifyPropertyChanged("IsActive");
NotifyPropertyChanged("IcoOpacity");
}
}
public bool Enabled
{
get => _enabled;
set
{
if (_enabled == value) return;
_enabled = value;
NotifyPropertyChanged("Enabled");
}
}
public string TestNote
{
get => _testNote;
set
{
if (_testNote == value) return;
_testNote = value;
NotifyPropertyChanged("TestNote");
}
}
public TimeSpan TestTime
{
get => _testTime;
set
{
if (_testTime == value) return;
_testTime = value;
NotifyPropertyChanged("TestTime");
}
}
protected Recording()
{
TestName = "Unkonwn";
TestNote = "";
_testTime = new TimeSpan(0, 0, 0);
}
protected Recording(string testName, string testNote, TimeSpan testTime)
{
TestName = testName;
TestNote = testNote;
_testTime = testTime;
}
public string OneLineSummary => $"{TestName}, finished: "
+ TestTime;
private void NotifyPropertyChanged(string propertyName = "")
{
var handler = PropertyChanged;
handler?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public abstract bool playTest();
}
}
I tried add DataContext in XAML (postet in origin question), because of intellisence like this:
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
xmlns:dvm="clr-namespace:XamarinTest.ViewModel"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
d:DataContext="{system:Type dvm:RecordingViewModel}"
and this to Grid:
<Label Text="{Binding Recordings[0].TestName}" Grid.Row="0" Grid.Column="2" />
IntelliSence is OK, but text doesn´t show in app.
Finally is working!
XAML should looks like code below.
Imporant is xmls:viewModel="..." and <ContentPage.BindingContext>...</>.
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewModel="clr-namespace:XamarinTest.ViewModel;assembly=XamarinTest"
x:Class="XamarinTest.MainPage"
>
<ContentPage.BindingContext>
<viewModel:RecordingViewModel/>
</ContentPage.BindingContext>
<ListView x:Name="listView" ItemsSource="{Binding Recordings}" Grid.Row="1" Grid.Column="1">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ViewCell.View>
<StackLayout Orientation="Horizontal">
<Image Source="Accept" WidthRequest="40" HeightRequest="40" />
<StackLayout Orientation="Vertical" HorizontalOptions="StartAndExpand">
<Label Text="{Binding TestName}" HorizontalOptions="FillAndExpand" />
<Label Text="{Binding TestNote}" />
</StackLayout>
</StackLayout>
</ViewCell.View>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</ContentPage>
and MainPage.xaml.cs is okey
namespace XamarinTest
{
public partial class MainPage : ContentPage
{
public MainPage()
{
this.InitializeComponent();
this.AllTestViewModel = new RecordingViewModel();
this.BindingContext = AllTestViewModel;
}
public RecordingViewModel AllTestViewModel { get; set; }
}
}
looking at your ViewModel, it looks like there is no Recording member, but you do have a Recordings member.
EDIT
So you are adding your DataContext in the code behind so ignore the Xaml part.
Your View (MainPage.xaml) has a ViewModel(RecordingViewModel.cs). The ViewModel has a member called Recordings (a collection of type Recording). But in your Xaml, you are try to bind to Recording.
Change:
<ListView x:Name="listView" IsVisible="false" ItemsSource="{Binding Recording}">
to:
<ListView x:Name="listView" IsVisible="false" ItemsSource="{Binding Recordings}">
2nd EDIT
The only Labels in your example is the one inside of the ListView yes?
If so, you can access the Recordings children like TestNote by:
I have got a View who's DataContext is set to an Employee.
Further, the view uses a BindingGroup and Validation Rules.
At last the view has got 2 Buttons: Save and Cancel
Save: Validate the users input and in case of success, save the changes.
Cancel: Rollback the user input and restore the original values.
Until this point it works fine.
Now the last requirement and the problem:
For a better User Experience i would like to enable the save Button when the user begins to change data.
To achieve this, I bind the IsDirty Property of the BindingGroup to the Enabled Property of the Button.
Unfortunately it doesn't work. The binding seems to be correct, but the user interface does not recognize the change of IsDirty.
Who can i solve this problem?
My Model:
public class EmployeeModel:ModelBase
{
private int _nr;
private string _firstname;
private string _lastname;
public int Nr
{
get
{
return _nr;
}
set
{
_nr = value;
OnChanged(nameof(Nr));
}
}
public string Firstname
{
get
{
return _firstname;
}
set
{
_firstname = value;
OnChanged(nameof(Firstname));
}
}
public string Lastname
{
get
{
return _lastname;
}
set
{
_lastname = value;
OnChanged(nameof(Lastname));
}
}
}
ModelBase:
public class ModelBase:INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnChanged(string propertyname)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyname));
}
}
ValidationRule:
public class EmployeeValidationRule:ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
BindingGroup bindingGroup = (BindingGroup)value;
if (bindingGroup.Items.Count == 2)
{
EmployeeModel employee = (EmployeeModel)bindingGroup.Items[1];
string firstname = (string)bindingGroup.GetValue(employee, "Firstname");
string lastname = (string)bindingGroup.GetValue(employee, "Lastname");
if (firstname.Length == 0)
return new ValidationResult(false, "Firstname can not be empty.");
if (lastname.Length == 0)
return new ValidationResult(false, "Lastname can not be empty.");
}
return ValidationResult.ValidResult;
}
}
My ViewModel:
public class EmployeeViewModel
{
private EmployeeModel _employeeModel;
public EmployeeModel Employee
{
get
{
return _employeeModel;
}
set
{
_employeeModel = value;
}
}
public EmployeeViewModel()
{
LoadData();
}
private void LoadData()
{
//Employee = (from e in _context.Employee
// where e.Nr == 158
// select e).FirstOrDefault();
Employee = new EmployeeModel() { Firstname = "Billy", Lastname = "Wilder" };
}
public void Save()
{
//_context.SaveChanges();
}
}
At last the View:
<Window x:Class="WpfApplication3_Validation.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:WpfApplication3_Validation"
xmlns:vm="clr-namespace:WpfApplication3_Validation.ViewModel"
xmlns:vr="clr-namespace:WpfApplication3_Validation.ValidationRules"
mc:Ignorable="d"
Title="Employee" Height="250" Width="525"
Validation.ValidationAdornerSite="{Binding ElementName=lbErrors}" Loaded="Window_Loaded">
<Window.DataContext>
<vm:EmployeeViewModel/>
</Window.DataContext>
<Window.BindingGroup>
<BindingGroup x:Name="MyBindingGroup">
<BindingGroup.ValidationRules>
<vr:EmployeeValidationRule/>
</BindingGroup.ValidationRules>
</BindingGroup>
</Window.BindingGroup>
<Grid x:Name="gridMain">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Label Content="Nr:"/>
<TextBlock Grid.Column="1" Text="{Binding Employee.Nr}"/>
<Label Grid.Row="1" Content="Vorname:" Target="{Binding ElementName=tbFirstname}"/>
<TextBox Grid.Row="1" Grid.Column="1" x:Name="tbFirstname" Text="{Binding Employee.Firstname}"/>
<Label Grid.Row="2" Content="Nachname:" Target="{Binding ElementName=tbLastname}"/>
<TextBox Grid.Row="2" Grid.Column="1" x:Name="tbLastname" Text="{Binding Employee.Lastname}"/>
<Label Grid.Row="4" Grid.Column="0" x:Name="lbErrors" Content="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.ValidationAdornerSiteFor).(Validation.Errors)[0].ErrorContent}"
Foreground="Red" FontWeight="Bold"/>
<StackPanel Grid.Row="4" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
<TextBlock x:Name="tbIsDirty"/>
<Button x:Name="btn1" Content="IsDirty?" Click="btn1_Click"/>
<Button x:Name="btnSave" Content="Save1" Click="btnSave_Click" />
<Button x:Name="btnSave1" Content="Save2" Click="btnSave_Click" IsEnabled="{Binding ElementName=MyBindingGroup, Path=IsDirty}"/>
<Button x:Name="btnCancel" Content="Cancel" Click="btnCancel_Click"/>
</StackPanel>
</Grid>
Code Behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
this.MyBindingGroup.BeginEdit(); // Not really needed?
}
private void btnSave_Click(object sender, RoutedEventArgs e)
{
if (this.BindingGroup.CommitEdit())
{
EmployeeViewModel vm = (EmployeeViewModel)this.DataContext;
vm.Save();
}
}
private void btnCancel_Click(object sender, RoutedEventArgs e)
{
this.BindingGroup.CancelEdit();
}
private void btn1_Click(object sender, RoutedEventArgs e)
{
tbIsDirty.Text = BindingGroup.IsDirty.ToString();
}
}
Due to the fact that BindingGroup.IsDirty does not Implement INotifyPropertyChanged, it's not a useful source for this type of databinding.
Possible solution:
- Implementing INotifyPropertyChanged in the view
- Creating a own IsDirty in the view, using INotifyPropertyChanged
- Adding an event handler for KeyUp, which sets my IsDirty in case of BindingGroup.IsDirty.
- Binding of Enabled to the new Property
Disadvantage: Need if implementation of INotifyPropertyChanged in the view.
Advantage: It works.
CodeBehind of View:
public partial class MainWindow : Window, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnChanged(string propertyname)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyname));
}
private bool _isDirty;
public bool IsDirty
{
get
{
return _isDirty;
}
set
{
_isDirty = value;
OnChanged(nameof(IsDirty));
}
}
public MainWindow()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
this.MyBindingGroup.BeginEdit(); // Not really needed?
gridMain.KeyUp += GridMain_KeyUp;
}
private void GridMain_KeyUp(object sender, System.Windows.Input.KeyEventArgs e)
{
if (this.MyBindingGroup.IsDirty)
{
IsDirty = true;
}
}
private void btnSave_Click(object sender, RoutedEventArgs e)
{
if (this.BindingGroup.CommitEdit())
{
EmployeeViewModel vm = (EmployeeViewModel)this.DataContext;
vm.Save();
IsDirty = false;
}
}
private void btnCancel_Click(object sender, RoutedEventArgs e)
{
this.BindingGroup.CancelEdit();
IsDirty = false;
}
}
Further improvements:
Now i moved IsDirty to my ViewModel, so I don't have to implement INPC in the view. Another advantage is, that in this way, Commands can consume the property and finally i don't have to use databinding for the enabled Property, because i get it over the command.
This is my first experience with C#, as well as .net in general. I'm trying to get an understanding of MVVM and how it can work with a screen I want to build.
My button text is not updating when I am changing it after a different button is clicked. The debugger is showing that even though the OnPropertyChanged function is getting called, the PropertyChanged is always null.
I've looked at some documentation and taken a look at other posts of people asking similar questions but none of those solutions have worked for me. I'm also looking for an explanation so I can understand what i'm doing wrong.
Code:
MonthViewPage.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="Mobile_Release_POC_4.MonthViewPage"
xmlns:local="clr-namespace:Mobile_Release_POC_4;assembly=Mobile_Release_POC_4"
xmlns:telerikInput="clr-namespace:Telerik.XamarinForms.Input;assembly=Telerik.XamarinForms.Input"
xmlns:sys="clr-namespace:System;assembly=mscorlib">
<ContentPage.BindingContext>
<local:WeekViewDatesViewModel MiddleWeekViewDate='{x:Static sys:DateTime.Now}' />
</ContentPage.BindingContext>
<StackLayout>
<Label Text="Monday, April 27, 2015"
HorizontalOptions="Center"></Label>
<Grid Padding="0" ColumnSpacing="-2">
<Grid.RowDefinitions>
<RowDefinition Height="75" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions >
<ColumnDefinition Width="50" />
</Grid.ColumnDefinitions>
<Button x:Name="monthViewDateButton1"
Text="{Binding MiddleWeekViewDate}"
FontSize="10"
Grid.Row="0"
Grid.Column="0"
/>
<Button x:Name="monthViewDateButton2"
Text="Tue 4/28"
FontSize="10"
Grid.Row="0"
Grid.Column="1"
Clicked="OnButtonClicked"
/>
</Grid>
</StackLayout>
MonthViewPage.xaml.cs
using Xamarin.Forms;
namespace Mobile_Release_POC_4
{
public partial class MonthViewPage : ContentPage
{
public MonthViewPage ()
{
InitializeComponent();
}
void OnButtonClicked(object sender , EventArgs args){
WeekViewDatesViewModel dc = new WeekViewDatesViewModel ();
dc.MiddleWeekViewDate = new DateTime (2014, 2, 2);
}
}
WeekViewDatesViewModel.cs
namespace Mobile_Release_POC_4
{
public class WeekViewDatesViewModel : INotifyPropertyChanged
{
DateTime middleWeekViewDate;
public event PropertyChangedEventHandler PropertyChanged;
public DateTime MiddleWeekViewDate
{
set
{
if (middleWeekViewDate != value)
{
middleWeekViewDate = value;
OnPropertyChanged("MiddleWeekViewDate");
}
}
get
{
return middleWeekViewDate;
}
}
protected void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (PropertyChanged != null)
{
PropertyChanged(this,
new PropertyChangedEventArgs(propertyName));
}
}
}
}
Thank you
Only create one ViewModel, its scope is the life of the page (at least).
Change your code to this:
public partial class MonthViewPage : ContentPage
{
public WeekViewDatesViewModel VM { get; set; }
public MonthViewPage ()
{
DataContext = VM = new WeekViewDatesViewModel();
InitializeComponent();
}
void OnButtonClicked(object sender , EventArgs args){
VM.MiddleWeekViewDate = new DateTime (2014, 2, 2);
}
}