I have two types of users (user/stylist and stylist inherits from user) and in profile page I need to build it different so I'm using data template selector but the view is not calling the selector
Here is the view
<ContentPage.Resources>
<DataTemplate x:Key="UserTemplate">
<StackLayout>
<Label Text="I'm a user"/>
</StackLayout>
</DataTemplate>
<DataTemplate x:Key="StylistTemplate">
<StackLayout BackgroundColor="DarkBlue">
<Label Text="I'm a stylist"/>
</StackLayout>
</DataTemplate>
<local:UserTypeSelector
x:Key="personDataTemplateSelector"
StylistTemplate="{StaticResource StylistTemplate}"
UserTemplate="{StaticResource UserTemplate}" />
</ContentPage.Resources>
<ContentPage.Content>
<StackLayout
Margin="10"
BindableLayout.ItemTemplateSelector="{StaticResource personDataTemplateSelector}"
BindableLayout.ItemsSource="{Binding User}" />
</ContentPage.Content>
The selector class
IMPORTANT
I debugged it and the OnSelectTemplate it's not called at all
public class UserTypeSelector : DataTemplateSelector
{
public DataTemplate UserTemplate { get; set; }
public DataTemplate StylistTemplate { get; set; }
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
{
//return ((User)item).IsStylist ? StylistTemplate : UserTemplate
return StylistTemplate; this is for testing the selector
}
}
The code behind
public partial class ProfilePage : ContentPage
{
public ProfilePage()
{
InitializeComponent();
BindingContext = new ProfileViewModel();
}
}
And the view model
private User user { get; set; }
public User User { get; set; }
public ProfileViewModel()
{
//User = new User()
//{
// IsStylist = true, I tried to use this to test the selector but it throws
//}; an error **Object must implement IConvertible**
}
BindableLayout.ItemsSource should specifie the collection of IEnumerable items to be displayed by the layout.
So i modify your viewmodel,then it will work.
ProfileViewModel:
class ProfileViewModel
{
public ObservableCollection<User> Users { get; set; }
public ProfileViewModel()
{
Users = new ObservableCollection<User>()
{
new User(){ IsStylist = false},new User(){ IsStylist = false}
};
}
public class User
{
public bool IsStylist { get; set; }
}
}
the xaml :
<ContentPage.Resources>
<DataTemplate x:Key="UserTemplate">
<StackLayout>
<Label Text="I'm a user"/>
</StackLayout>
</DataTemplate>
<DataTemplate x:Key="StylistTemplate">
<StackLayout BackgroundColor="DarkBlue">
<Label Text="I'm a stylist"/>
</StackLayout>
</DataTemplate>
<local:UserTypeSelector
x:Key="personDataTemplateSelector"
StylistTemplate="{StaticResource StylistTemplate}"
UserTemplate="{StaticResource UserTemplate}" />
</ContentPage.Resources>
<ContentPage.Content>
<StackLayout
Margin="10"
BindableLayout.ItemTemplateSelector="{StaticResource personDataTemplateSelector}"
BindableLayout.ItemsSource="{Binding Users}" />
</ContentPage.Content>
Related
I am trying to learn MAUI in order to create a project, but it seems like i got stuck. I cant understand the MVVM architecture, as i never had any simillar experience before. I will now present my code, and would love to get answers that could explain why it is not working, and a possible solution for the problem.
I have three folders: Views, where i store the design. Models, where i store the classes. And ViewModels, that get the data. This is the xaml content page, which consists of a login page.
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="thebridgeproject.Views.login"
xmlns:ViewModels="clr-namespace:thebridgeproject.ViewModels"
Shell.NavBarIsVisible="False"
Title="LoginPage" >
<ContentPage.BindingContext>
<ViewModels:LoginViewModel />
</ContentPage.BindingContext>
<VerticalStackLayout
Spacing="25"
Padding="30,0"
VerticalOptions="Center">
<Image Source="loginicon.png" HeightRequest="150" WidthRequest="150" />
<VerticalStackLayout Spacing="5">
<Label Text="Welcome!" FontSize="28" TextColor="#3B7A5E" HorizontalTextAlignment="Center" />
<Label Text="Login to your account" FontSize="18" TextColor="Gray" HorizontalTextAlignment="Center" />
</VerticalStackLayout>
<StackLayout Orientation="Horizontal">
<Frame ZIndex="1" HasShadow="True" BorderColor="White" HeightRequest="56" WidthRequest="56" CornerRadius="28">
<Image Source="user.png" HeightRequest="20" WidthRequest="20" />
</Frame>
<Frame HeightRequest="45" Margin="-20,0,0,0" Padding="0" HasShadow="True" BorderColor="White" HorizontalOptions="FillAndExpand">
<Entry Text="{Binding Username}" Margin="20,0,0,0" VerticalOptions="Center" Placeholder="Username"/>
</Frame>
</StackLayout>
<StackLayout Orientation="Horizontal">
<Frame ZIndex="1" HasShadow="True" BorderColor="White" HeightRequest="56" WidthRequest="56" CornerRadius="28">
<Image Source="lock.png" HeightRequest="20" WidthRequest="20" />
</Frame>
<Frame HeightRequest="45" Margin="-20,0,0,0" Padding="0" HasShadow="True" BorderColor="White" HorizontalOptions="FillAndExpand">
<Entry Text="{Binding Password}" Margin="20,0,0,0" VerticalOptions="Center" Placeholder="Password" IsPassword="True" />
</Frame>
</StackLayout>
<Button Text="Sign in" WidthRequest="100" CornerRadius="20" HorizontalOptions="Center" BackgroundColor="#3B7A5E" Command="{Binding LoginCommand}" />
<StackLayout Orientation="Horizontal" Spacing="5" HorizontalOptions="Center">
<Label Text="Dont have an account?" TextColor="Gray" />
<Label Text="Sign up here" TextColor="#50b3f2" />
</StackLayout>
</VerticalStackLayout>
</ContentPage>
Then i have the Model that holds the data for the API request.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace thebridgeproject.Models
{
class users
{
public class Result
{
public int NumUtente { get; set; }
public string Nome { get; set; }
public string Password { get; set; }
public string Morada { get; set; }
public string Cidade { get; set; }
public string DataNascimento { get; set; }
public string NumTlf { get; set; }
}
public class Root
{
public bool success { get; set; }
public string message { get; set; }
public List<Result> result { get; set; }
}
}
}
After that, we have the LoginViewModel:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using thebridgeproject.Models;
namespace thebridgeproject.ViewModels
{
public class LoginViewModel : INotifyPropertyChanged
{
private string _username;
public string Username
{
get { return _username; }
set
{
_username = value;
OnPropertyChanged(nameof(Username));
}
}
private string _password;
public string Password
{
get { return _password; }
set
{
_password = value;
OnPropertyChanged(nameof(Password));
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private async Task Login()
{
using (var httpClient = new HttpClient())
{
httpClient.DefaultRequestHeaders.Add("Authorization", "RaV9N");
var response = await httpClient.GetAsync("http:///bp/utentes");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var users = JsonConvert.DeserializeObject<users.Root>(content);
if (users.success)
{
var user = users.result.FirstOrDefault(x => x.Nome == Username);
if (user != null && VerifyPassword(user.Password, Password))
{
// Login successful
// ...
}
else
{
// Login failed
// ...
}
}
else
{
// API request failed
// ...
}
}
else
{
// API request failed
// ...
}
}
}
private bool VerifyPassword(string hashedPassword, string enteredPassword)
{
// Use the BCrypt.Net library to verify the entered password
return BCrypt.Net.BCrypt.Verify(enteredPassword, hashedPassword);
}
}
}
Ignore the API link! But that is mostly it, i have no more code. It seems like it does nothing. I think the issue might be the lack of code in the file behind the design. Im open to suggestions, and i am thankfull for any productive answer!
The view model implements properties and commands to which the view can data bind to, and notifies the view of any state changes through change notification events. The view model is also responsible for coordinating the view's interactions with any model classes that are required. There's typically a one-to-many relationship between the view model and the model classes. You can refer to The Model-View-ViewModel Pattern for more details.
You can refer to my sample code below on how to use LoginCommand in your LoginCommand. Notice that I put it in a Label TapGestureRecognizer, however its usage is the same as that you use it in a Button.
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" xmlns:ViewModel="clr-namespace:MyApp.ViewModels"
xmlns:local="clr-namespace:MyApp"
x:Class="MyApp.Views.LoginPage"
BackgroundColor="#112B47"
>
<ContentPage.BindingContext>
<ViewModel:LoginViewModel/>
</ContentPage.BindingContext>
<StackLayout Padding="15" VerticalOptions="Center" HorizontalOptions="FillAndExpand">
<Label HorizontalOptions="Center">
<Label.FormattedText>
<FormattedString>
<Span Text="Don't have an account?" TextColor="Gray"></Span>
<Span Text="Register" TextColor="Gray" FontAttributes="Bold" TextDecorations="Underline"></Span>
</FormattedString>
</Label.FormattedText>
<Label.GestureRecognizers>
<TapGestureRecognizer Command="{Binding LoginCommand}"></TapGestureRecognizer>
</Label.GestureRecognizers>
</Label>
</StackLayout>
</ContentPage>
LoginViewModel:
public class LoginViewModel : INotifyPropertyChanged
{
public ICommand LoginCommand { get; private set; }
public event PropertyChangedEventHandler PropertyChanged;
public LoginViewModel()
{
LoginCommand = new Command(async () => await Login());
}
public async Task Login()
{
// add your logic here
}
}
I have:
<Grid.GestureRecognizers>
<TapGestureRecognizer NumberOfTapsRequired="1" Command="{Binding Source={RelativeSource AncestorType={x:Type locals:OneViewModel}},
Path=OneTappedView}" CommandParameter="{Binding .}" />
</Grid.GestureRecognizers>
<Button x:Name="bt_one" Clicked="bt_one_Clicked"/>
When I do Grid Tap, Command and bt_one_Clicked execute concurrently? Thank you
When I do Grid Tap, Command and bt_one_Clicked execute concurrently?
Yes, you can add the button's clicked code in your grid tap event when tapping your grid.
You can refer to the following code:
OnePage.xml
<?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:formapp1="clr-namespace:FormApp1"
x:Class="FormApp1.OnePage">
<ContentPage.BindingContext>
<formapp1:OneViewModel></formapp1:OneViewModel>
</ContentPage.BindingContext>
<ContentPage.Content>
<StackLayout>
<Grid WidthRequest="300" HeightRequest="600" BackgroundColor="Yellow">
<Grid.GestureRecognizers>
<TapGestureRecognizer NumberOfTapsRequired="1" Command="{Binding OneTappedViewCommand}" />
</Grid.GestureRecognizers>
</Grid>
<Button x:Name="bt_one" Text="one button" Command="{Binding BtnOneClickedCommand}"/>
</StackLayout>
</ContentPage.Content>
</ContentPage>
OneViewModel.cs
public class OneViewModel: INotifyPropertyChanged
{
public ICommand OneTappedViewCommand { private set; get; }
public ICommand BtnOneClickedCommand { private set; get; }
public OneViewModel() {
OneTappedViewCommand = new Command(GridTapped);
BtnOneClickedCommand= new Command(btnOneClickedMthod);
}
private void GridTapped()
{
System.Diagnostics.Debug.WriteLine("----111------> GridTapped is triggered......");
//add one clicked method here
btnOneClickedMthod();
}
private void btnOneClickedMthod()
{
System.Diagnostics.Debug.WriteLine("----222------> GridTapped is triggered......");
}
bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (Object.Equals(storage, value))
return false;
storage = value;
OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
Note:
In model OneViewModel, you can add bt_one event(btnOneClickedMthod) in grid tap function GridTapped.
i want set name for check box and use in code for post method for api
<ListView ItemsSource="{Binding}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout BackgroundColor="#eee" Orientation="Vertical">
<StackLayout Orientation="Horizontal">
<controls:CheckBox DefaultText="{Binding Name}" />
</StackLayout>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Since you had used MVVM . I suggest that you should handle all logic in your ViewModel .You can get the value and index of CheckBox in ViewModel.
I used the CheckBox plugin from https://github.com/enisn/Xamarin.Forms.InputKit .
in your xaml
<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"
xmlns:local="clr-namespace:App12"
xmlns:input="clr-namespace:Plugin.InputKit.Shared.Controls;assembly=Plugin.InputKit"
mc:Ignorable="d"
x:Name="contentPage" // set the name of content page
x:Class="xxx.MainPage">
<ListView x:Name="listview" ItemsSource="{Binding MyItems}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout BackgroundColor="#eee" Orientation="Vertical">
<StackLayout Orientation="Horizontal">
<input:CheckBox Text="{Binding Name}" Type="Check" IsChecked="{Binding IsCheck,Mode=TwoWay}" CheckChangedCommand="{Binding Source={x:Reference contentPage}, Path=BindingContext.CheckCommand}" CommandParameter="{Binding }"/>
</StackLayout>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
in your model
public class Model : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public string Name { get; set; }
private bool isCheck;
public bool IsCheck
{
get
{
return isCheck;
}
set
{
if (isCheck != value)
{
isCheck = value;
NotifyPropertyChanged();
}
}
}
}
in Viewmodel or Code behind
public ObservableCollection<Model> MyItems { get; set; }
public ICommand CheckCommand { get; private set; }
public YourViewModel()
{
MyItems = new ObservableCollection<Model>() {
new Model(){Name="xxx",IsCheck=true },
//...
};
CheckCommand = new Command((arg)=> {
var model = arg as Model;
for(int i=0;i<MyItems.Count;i++)
{
if (model == MyItems[i])
{
// i is the index that you checked
bool ischeck = MyItems[i].IsCheck;
// do some thing you want
}
}
});
}
I would suggest adding a Binding for the CheckBox State:
<controls:CheckBox x:Name="chechBox" DefaultText="{Binding Name}" IsChecked="{Binding IsChecked}" />
And then, in the ListView ItemTapped event:
void OnSelection (object sender, SelectedItemChangedEventArgs e)
{
if (e.SelectedItem == null) {
return; //ItemSelected is called on deselection, which results in SelectedItem being set to null
}
var item = (YourModel)e.SelectedItem;
if(item != null)
{
var checkBoxState = item.IsChecked;
}
}
I have a custom control that I supply a List<string> parameter and it draws a box for each string as a label.
Here is a version that is working. Clicking 'Add Tab' adds one tab at a time for each click.
I want to change the code so the List is converted into a different type of object and that is what the control displays.
First I show the code that is working for the image above. Then I show a changed version of the code that I am unable to get working. Hopefully for anyone answering this question, seeing the before code that works and the after code that doesn't work, you can easily spot the issue.
MainPage.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"
xmlns:local="clr-namespace:App7"
x:Class="App7.MainPage">
<StackLayout>
<BoxView HeightRequest="100" />
<local:CustomControl
SideTabs="{Binding MainNavigationTabs}"
/>
<Button Text="Add Tab" Command="{Binding AddTab}" />
</StackLayout>
</ContentPage>
MainPageModel.cs
public class MainPageModel : FreshBasePageModel
{
public MainPageModel() { }
public List<string> MainNavigationTabs { get; set; }
private int _index = 0;
public Command AddTab
{
get
{
return new Command(() =>
{
_index++;
var tabs = new List<string>();
for (var index = 1; index <= _index; index++)
{
tabs.Add($"Tab {index}");
}
MainNavigationTabs = tabs;
});
}
}
}
CustomControl.xaml
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="App7.CustomControl"
BackgroundColor="Beige"
x:Name="this">
<ContentView.Content>
<StackLayout>
<StackLayout Orientation="Vertical"
BindableLayout.ItemsSource="{Binding Source={x:Reference this}, Path=SideTabs}">
<BindableLayout.ItemTemplate>
<DataTemplate>
<ContentView WidthRequest="237"
Margin="0"
BackgroundColor="Blue"
Padding="10">
<Label Text="{Binding .}" TextColor="White" />
</ContentView>
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
</StackLayout>
</ContentView.Content>
</ContentView>
CustomControl.xaml.cs
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class CustomControl : ContentView
{
public CustomControl()
{
InitializeComponent();
}
public static readonly BindableProperty SideTabsProperty = BindableProperty.Create(
propertyName: "SideTabs",
returnType: typeof(List<string>),
declaringType: typeof(CustomControl),
defaultBindingMode: BindingMode.OneWay,
defaultValue: new List<string>());
public List<string> SideTabs
{
get { return base.GetValue(SideTabsProperty) as List<string>; }
set { base.SetValue(SideTabsProperty, value); }
}
}
I changed the CustomControl to transform the List<string> to a List<SideTab> object and have the control bind to that. Here's the code...
CustomControl.xaml.cs
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class CustomControl : ContentView
{
public CustomControl()
{
InitializeComponent();
}
public static readonly BindableProperty SideTabsProperty = BindableProperty.Create(
propertyName: "SideTabs",
returnType: typeof(List<string>),
declaringType: typeof(CustomControl),
defaultBindingMode: BindingMode.OneWay,
defaultValue: new List<string>());
public List<string> SideTabs
{
get
{
var tabs = new List<string>();
foreach (var tab in _SideTabs)
{
tabs.Add(tab.Text);
}
return tabs;
}
set
{
var tabs = new List<SideTab>();
foreach (var tab in value)
{
tabs.Add(new SideTab() { Text = tab });
}
_SideTabs = tabs;
}
}
public List<SideTab> _SideTabs
{
get { return base.GetValue(SideTabsProperty) as List<SideTab>; }
set { base.SetValue(SideTabsProperty, value); }
}
}
public class SideTab
{
public string Text { get; set; }
}
CustomControl.xaml
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="App7.CustomControl"
BackgroundColor="Beige"
x:Name="this">
<ContentView.Content>
<StackLayout>
<StackLayout Orientation="Vertical"
BindableLayout.ItemsSource="{Binding Source={x:Reference this}, Path=_SideTabs}">
<BindableLayout.ItemTemplate>
<DataTemplate>
<ContentView WidthRequest="237"
Margin="0"
BackgroundColor="Blue"
Padding="10">
<Label Text="{Binding Text}" TextColor="White" />
</ContentView>
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
</StackLayout>
</ContentView.Content>
</ContentView>
Notice the addition of a property _SideTabs. When SideTabs is set, it transforms the List<string> into a List<SideTab>.
How can I make this work? Here is the result from the above code changes...
Try like this,
public static readonly BindableProperty TabsListProperty = BindableProperty.Create(nameof(TabsList), typeof(List<TabItem>), typeof(ScrollableTabs), null, propertyChanged: (bindable, oldValue, newValue) =>
{
((ScrollableTabs)bindable).InitializeTabs();
});
private void InitializeTabs()
{
//Write your logic here
}
public List<TabItem> TabsList
{
get
{
return (List<TabItem>)GetValue(TabsListProperty);
}
set
{
SetValue(TabsListProperty, value);
}
}
I'm trying to have a TextBox's content be validated using IDataErrorInfo. The source of the list below is a List and each item is display. When i put ValidatesOnDataErrors=True in the Binding for the Text on the TextBox, it's not working as expected. How do I do this?
<ItemsControl ItemsSource="{Binding Trainings}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<StackPanel>
<TextBlock Text="{Binding MobileOperator}" />
<TextBlock Text="{Binding LastUpdate}"/>
</StackPanel>
<StackPanel>
<TextBlock Text="Number trained*" />
<!-- ValidatesOnDataErrors doesn't work here-->
<TextBox
Text="{Binding NumberTrained,
ValidatesOnDataErrors=True}"/>
</StackPanel>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Update: Posting a stripped down version of Model, ViewModel, View and CodeBehind
ViewModel and Model
public class MyViewModel : IDataErrorInfo, INotifyPropertyChanged
{
public MyViewModel()
{
Trainings = new List<MyModel>
{
new MyModel { NumberTrained = 5, MobileOperator = "MO 1", LastUpdate = DateTime.Now },
new MyModel { NumberTrained = 1, MobileOperator = "MO 2", LastUpdate = DateTime.Now },
};
OkButtonCommand = new ButtonCommand(OnClick);
}
private void OnClick()
{
PropertyChanged(this, new PropertyChangedEventArgs(""));
}
public event PropertyChangedEventHandler PropertyChanged;
public ICommand OkButtonCommand { get; private set; }
public List<MyModel> Trainings { get; private set; }
public string Error { get { return null; } }
public string this[string columnName]
{
get
{
string error = null;
switch (columnName)
{
case "NumberTrained":
error = "error from IDataErrorInfo";
break;
}
return error;
}
}
}
public class MyModel
{
public string MobileOperator { get; set; }
public DateTime LastUpdate { get; set; }
public int NumberTrained { get; set; }
}
public class ButtonCommand : ICommand
{
private Action _handler;
public event EventHandler CanExecuteChanged;
public ButtonCommand(Action handler) { _handler = handler; }
public bool CanExecute(object parameter) { return true; }
public void Execute(object parameter) { _handler(); }
}
Code Behind
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
DataContext = new MyViewModel();
}
}
View
<Canvas x:Name="LayoutRoot" Background="White">
<ItemsControl ItemsSource="{Binding Trainings}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel HorizontalAlignment="Center">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<TextBlock Text="{Binding MobileOperator}" Margin="15,15,0,0" FontWeight="Bold"/>
<TextBlock Text="{Binding LastUpdate, StringFormat=' - Last Updated: \{0:M/d/yy\}'}"
Margin="5,15,15,0" Foreground="Gray"/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<TextBlock Text="Number trained*" />
<TextBox Width="50" Height="20"
Text="{Binding NumberTrained, Mode=TwoWay, ValidatesOnExceptions=True, ValidatesOnDataErrors=True}"/>
</StackPanel>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Content="ok" Width="100" Height="20" Canvas.Left="248" Canvas.Top="207" Command="{Binding OkButtonCommand}"/>
</Canvas>
I feel implementing IDataErrorInfo on ViewModel is more appropriate rather than on Model.
So in your case, you could have created an additional ViewModel (ex: MyModelViewModel) and include it as a List<MyModelViewModel> inside MyViewModel to be used as the ItemsSource.
Going by MVVM, if you feel you should have a corresponding View for it, you can extract out the DataTemplate of the ItemsControl to a new XMAL.
You need to implement IDataErrorInfo on your Model, not your ViewModel.
As it is right now, your a validation check is throwing an error when you try and validate the property MyViewModel.NumberTrained, which doesn't exist, so the validation error never gets called.