I wrote a simple dictionary app with a single XAML page, then I decided to put the entries and translations into separate views with their own ViewmModels and the fun began.
Basically I managed to make everything work, but it just shows me these nagging XAML binding errors, I don't know why, AI went stupid on this, would much appreciate help!
EntryModel has properties
Header
andSubheader
TranslationModel has its own properties
Errors look like this:
Severity Count Data Context Binding Path Target Target Type Description File Line Project
Error 50 EntryViewModel Header Label.Text String 'Header' property not found on 'dosham.ViewModels.EntryViewModel', target property: 'Microsoft.Maui.Controls.Label.Text' C:\Users\x.dr\source\repos\movsar\chldr\src\chldr_maui\Views\EntryView.xaml 11 dosham
Error 50 EntryViewModel Subheader Label.Text String 'Subheader' property not found on 'dosham.ViewModels.EntryViewModel', target property: 'Microsoft.Maui.Controls.Label.Text' C:\Users\x.dr\source\repos\movsar\chldr\src\chldr_maui\Views\EntryView.xaml 14 dosham
Error 50 EntryViewModel Translations StackLayout.ItemsSource IEnumerable 'Translations' property not found on 'dosham.ViewModels.EntryViewModel', target property: 'Microsoft.Maui.Controls.StackLayout.ItemsSource' C:\Users\x.dr\source\repos\movsar\chldr\src\chldr_maui\Views\EntryView.xaml 18 dosham
Error 50 EntryViewModel . EntryView.Entry EntryModel 'dosham.ViewModels.EntryViewModel' cannot be converted to type 'chldr_data.DatabaseObjects.Models.EntryModel' C:\Users\x.dr\source\repos\movsar\chldr\src\chldr_maui\Pages\MainPage.xaml 24 dosham
I see it's saying that it can't find the Header and Subheader on my ViewModel, but it shouldn't be looking for them on the ViewModel, instead on my Entry and Translation models from ViewModel. How do I tell that without breaking the bindings that are working right now?
Also, if you have better suggestions on code organization - you're welcome to advice.
Here's the code:
MainPage
-- View
<?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"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:vm="clr-namespace:dosham.ViewModels"
xmlns:views="clr-namespace:dosham.Views"
x:Class="dosham.Pages.MainPage">
<Grid Padding="10">
<!-- Define the rows -->
<Grid.RowDefinitions>
<!-- For CollectionView, takes remaining space -->
<RowDefinition Height="*"/>
<!-- For Search Box -->
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Collection View for Entries -->
<CollectionView x:Name="EntriesCollectionView" Grid.Row="0"
ItemsSource="{Binding FilteredEntries}">
<CollectionView.ItemTemplate>
<DataTemplate>
<views:EntryView Entry="{Binding .}"></views:EntryView>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<!-- Search Box -->
<Entry x:Name="SearchBox" Grid.Row="1"
Text="{Binding SearchText, Mode=TwoWay}"
Placeholder="Начните писать..."
Margin="0,0,0,10" />
</Grid>
</ContentPage>
-- Codebehind
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
BindingContext = App.Services.GetRequiredService<MainPageViewModel>();
}
}
-- ViewModel
public class MainPageViewModel : ReactiveObject
{
public IEnumerable<EntryModel> FilteredEntries
{
get => _filteredEntries;
set => this.RaiseAndSetIfChanged(ref _filteredEntries, value);
}
....
EntryView
-- View
<?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:views="clr-namespace:dosham.Views"
x:Class="dosham.Views.EntryView">
<StackLayout Orientation="Vertical">
<Frame CornerRadius="10" Margin="5" Padding="10" BorderColor="#CACCaa" BackgroundColor="#f9F9F9">
<StackLayout Orientation="Vertical" Spacing="5">
<!-- Entry Content -->
<Label Text="{Binding Header}" FontSize="Medium" FontAttributes="Bold" TextColor="#000" />
<!-- Source Name -->
<Label Text="{Binding Subheader}" FontSize="Micro" TextColor="#cba" />
<!-- Translation views -->
<StackLayout BindableLayout.ItemsSource="{Binding Translations}">
<BindableLayout.ItemTemplate>
<DataTemplate>
<views:TranslationView Translation="{Binding .}" />
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
</StackLayout>
</Frame>
</StackLayout>
</ContentView>
-- Codebehind
public partial class EntryView : ContentView
{
private EntryViewModel _viewModel;
public static readonly BindableProperty EntryProperty =
BindableProperty.Create(nameof(_viewModel.Entry), typeof(EntryModel), typeof(EntryView));
public EntryView()
{
_viewModel = App.Services.GetRequiredService<EntryViewModel>();
BindingContext = _viewModel;
InitializeComponent();
}
}
-- ViewModel
public class EntryViewModel : ViewModelBase
{
#region Properties, Actions and Constructors
public EntryModel Entry { get; set; }
...
}
TranslationView
-- View
<?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:local="clr-namespace:dosham.Views"
x:Class="dosham.Views.TranslationView">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Language Code Label -->
<Label Grid.Column="0"
Text="{Binding LanguageCode, StringFormat='{0}:'}"
FontSize="Small"
FontAttributes="Italic"
TextColor="#AA6666" />
<!-- Content Label -->
<Label Grid.Column="1"
Text="{Binding Content, StringFormat=' {0}'}"
FontSize="Small"
TextColor="#666666" />
</Grid>
</ContentView>
-- Codebehind
public partial class TranslationView : ContentView
{
private readonly TranslationViewModel _viewModel;
public static readonly BindableProperty TranslationProperty =
BindableProperty.Create(nameof(_viewModel.Translation), typeof(TranslationModel), typeof(TranslationView));
public TranslationView()
{
_viewModel = App.Services.GetRequiredService<TranslationViewModel>();
BindingContext = _viewModel;
InitializeComponent();
}
}
-- ViewModel
public class TranslationViewModel : ViewModelBase
{
public TranslationViewModel(ContentStore contentStore, UserStore userStore) : base(contentStore, userStore)
{ }
public TranslationModel Translation { get; set; }
....
}
After two weeks of suffering and frustration, I ended up with a genius solution - got rid of the EntryView and TranslationView converting both of them to DataTemplates for better performance, then created a single view to hold the whole thing - EntriesView with a EntriesViewModel.
I also changed ContentView to reactive:ReactiveContentView and ContentPage to reactive:ReactiveContentPage.
The resulting code looks like this:
MainPage
public partial class MainPage : ReactiveContentPage<MainPageViewModel>
{
public MainPage()
{
ViewModel = App.Services.GetRequiredService<MainPageViewModel>();
InitializeComponent();
// Reactive bindings
this.WhenActivated(disposable =>
{
this.OneWayBind(ViewModel, vm => vm.FilteredEntries, v => v.entriesView.ViewModel!.Entries);
});
}
}
EntriesView
<?xml version="1.0" encoding="utf-8" ?>
<reactive:ReactiveContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:viewModels ="clr-namespace:dosham.ViewModels"
xmlns:reactive="clr-namespace:ReactiveUI.Maui;assembly=ReactiveUI.Maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:TypeArguments="viewModels:EntriesViewModel"
x:Class="dosham.Views.EntriesView">
<CollectionView x:Name="EntriesCollection"
ItemTemplate="{StaticResource EntryTemplate}"
ItemsSource="{Binding Entries}">
</CollectionView>
</reactive:ReactiveContentView>
Codebehind
public partial class EntriesView : ReactiveContentView<EntriesViewModel>
{
public EntriesView()
{
ViewModel = App.Services.GetRequiredService<EntriesViewModel>();
InitializeComponent();
}
}
ViewModel (ViewModelBase inherits from ReactiveObject)
public class EntriesViewModel : ViewModelBase
{
private IEnumerable<EntryModel> _entries;
public IEnumerable<EntryModel> Entries
{
get => _entries;
set => this.RaiseAndSetIfChanged(ref _entries, value);
}
}
Works perfectly well :)