I am having an issue with laying out ItemTemplate in FlexLayout in maui. FlexLayout seems to act strange with laying out the item sources to it.
Here is a very simple code;
<DataTemplate x:Key="AndroidItemTemplate">
<Label Text="{Binding .}" FontSize="18" LineBreakMode="WordWrap"/>
</DataTemplate>
<ScrollView>
<FlexLayout JustifyContent="Start"
BindableLayout.ItemsSource="{Binding Sample}"
BindableLayout.ItemTemplate="{StaticResource AndroidItemTemplate}">
</FlexLayout>
</ScrollView>
MVVM with sets of string values;
public RangeObservableCollection<string> Sample { get; private set; } = new();
Sample.AddRange(new List<string>()
{
$"Hello, How are you",
$"Hello, How are you",
$"Hello, How are you",
$"Hello, How are you",
$"Hello, How are you",
$"Hello, How are you",
$"Hello, How are you",
$"Hello, How are you",
$"Hello, How are you",
$"Hello, How are you",
$"Hello, How are you",
$"Hello, How are you",
$"Hello, How are you",
$"Hello, How are you",
$"Hello, How are you",
$"Hello, How are you",
$"Hello, How are you",
$"Hello, How are you",
$"Hello, How are you",
$"Hello, How are you",
});
This is what is happening in Android I haven't tested it in iOS.
However, I want that the text should appear next to each other until the row ends. Then it should start with the 2nd row and go on.
Should Show:
Hello, How are you Hello, How
are you Hello, How are you
Hello, How are you Hello, how
are you Hello, how are you
Hello, how are you...
Bindable FlexLayout in MAUI is not Laying childs properly
The cause of this situation seems to be related to ViewModel.
Here is my code:
<ContentPage.Resources>
<DataTemplate x:Key="AndroidItemTemplate">
<Label Text="{Binding .}" FontSize="18" LineBreakMode="WordWrap"/>
</DataTemplate>
</ContentPage.Resources>
<ScrollView>
<FlexLayout JustifyContent="Start"
BindableLayout.ItemsSource="{Binding Sample}"
BindableLayout.ItemTemplate="{StaticResource AndroidItemTemplate}">
</FlexLayout>
</ScrollView>
Page.xaml.cs:
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent ();
BindingContext = new VM ();
}
}
ViewMdodel:
public class VM
{
public ObservableCollection<string> Sample { get; private set; } = new();
public VM()
{
gen();
}
void gen()
{
Sample.
Add("Hello, How are you Hello, How are you Hello, How are you Hello,"+
"How are you Hello,Hello, How are you Hello, How are you Hello,"+
"How are you Hello, How are you Hello, How are you Hello,");
//Sample.Add($"Hello, How are you");
//Sample.Add($"Hello, How are you");
//Sample.Add($"Hello, How are you");
//Sample.Add($"Hello, How are you");
//Sample.Add($"Hello, How are you");
//Sample.Add($"Hello, How are you");
//Sample.Add($"Hello, How are you");
}
}
When add only one item to Sample, it can display the effect that the text should appear next to each other until the row ends. Then it should start with the 2nd row and go on.
However, when you add more than one item to Sample, it will generate multiple labels in xaml. The effect you want cannot be achieved between labels, whether in FlexLayout or other layouts.
You have it almost right. Try this.
Page.xaml.cs
public partial class MainPage : ContentPage
{
public MainPage(VM viewmodel)
{
InitializeComponent ();
BindingContext = viewmodel;
}
}
in MauiProgram.cs add:
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>().ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
}).UseMauiCommunityToolkit().UseMauiCommunityToolkitMediaElement();
#if DEBUG
builder.Logging.AddDebug();
#endif
builder.Services.AddSingleton<VM>(); // ADD view model here
builder.Services.AddSingleton<Page>(); // ADD Page here
return builder.Build();
}
}
This is assuming you are using the MVVM model and are also using the community toolkit. But you need to add the above for dependancy injection.
Make sure to include the model, ViewModel, Class and data types at top of xaml page too like this.
xmlns:model="clr-namespace:YourProgramName.Model"
xmlns:viewmodel="clr-namespace:DirectoryOfViewModel.ViewModel"
x:Class="YourProgramName.View.Page"
x:DataType="viewmodel:PageViewModel"
adjust the above as needed for your program.
Here is a example of this in a Program I am developing atm.
TabletPodcastPage.xaml
<?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:model="clr-namespace:NerdNewsNavigator2.Model"
xmlns:viewmodel="clr-namespace:NerdNewsNavigator2.ViewModel.Tablet"
x:Class="NerdNewsNavigator2.View.Tablet.TabletPodcastPage"
x:DataType="viewmodel:TabletPodcastViewModel"
BackgroundColor="Wheat"
Shell.NavBarIsVisible="False"
Title="">
<Shell.BackButtonBehavior>
<BackButtonBehavior IsEnabled="True" IsVisible="False"></BackButtonBehavior>
</Shell.BackButtonBehavior>
<CollectionView ItemsSource="{Binding Podcasts}" Margin="5" BackgroundColor="Wheat" SelectionMode="None">
<CollectionView.ItemsLayout>
<GridItemsLayout Orientation="Vertical" Span="{Binding Orientation}"/>
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="model:Podcast">
<Grid RowDefinitions="30,400,*" ColumnDefinitions="400">
<Grid.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodel:TabletPodcastViewModel}}, Path=TapCommand}"
CommandParameter="{Binding Url}"/>
</Grid.GestureRecognizers>
<Label Grid.Row="0"
Text="{Binding Title}"
TextColor="Black"
HorizontalTextAlignment="Center"
FontSize="16"
FontAttributes="Bold"
LineBreakMode="WordWrap"/>
<Image Grid.Row="1"
Aspect="AspectFit"
MaximumHeightRequest="400"
MaximumWidthRequest="400">
<Image.Source>
<UriImageSource Uri="{Binding Image}"
CacheValidity="40"
CachingEnabled="True"/>
</Image.Source>
</Image>
<Label Grid.Row="2"
Margin="5"
Text="{Binding Description}"
FontSize="12"
TextColor="Black"
LineBreakMode="WordWrap"/>
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ContentPage>
TabletPodcastPage.xaml.cs
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace NerdNewsNavigator2.View.Tablet;
public partial class TabletPodcastPage : ContentPage
{
public TabletPodcastPage(TabletPodcastViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
}
protected override bool OnBackButtonPressed()
{
Application.Current.Quit();
return true;
}
}
MauiProgram.cs
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#if WINDOWS
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Windows.Graphics;
#endif
using NerdNewsNavigator2.Data;
namespace NerdNewsNavigator2;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>().ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
}).UseMauiCommunityToolkit().UseMauiCommunityToolkitMediaElement();
#if WINDOWS
builder.ConfigureLifecycleEvents(events =>
{
events.AddWindows(wndLifeCycleBuilder =>
{
wndLifeCycleBuilder.OnWindowCreated(window =>
{
window.ExtendsContentIntoTitleBar = false;
IntPtr hWnd = WinRT.Interop.WindowNative.GetWindowHandle(window);
WindowId myWndId = Win32Interop.GetWindowIdFromWindow(hWnd);
var appWindow = AppWindow.GetFromWindowId(myWndId);
appWindow.SetPresenter(AppWindowPresenterKind.FullScreen);
});
});
});
#endif
#if DEBUG
builder.Logging.AddDebug();
#endif
builder.Services.AddTransient<FirstPage>();
builder.Services.AddTransient<FirstVieModel>();
builder.Services.AddSingleton<PhonePodcastPage>();
builder.Services.AddSingleton<PhonePodcastViewModel>();
builder.Services.AddTransient<PhoneShowPage>();
builder.Services.AddTransient<PhoneShowViewModel>();
builder.Services.AddTransient<PhonePlayPodcastPage>();
builder.Services.AddTransient<PhonePlayPodcastViewModel>();
builder.Services.AddTransient<PhoneLivePage>();
builder.Services.AddTransient<PhoneLiveViewModel>();
builder.Services.AddSingleton<DesktopPodcastPage>();
builder.Services.AddSingleton<DesktopPodcastViewModel>();
builder.Services.AddTransient<DesktopShowPage>();
builder.Services.AddTransient<DesktopShowViewModel>();
builder.Services.AddTransient<DesktopPlayPodcastPage>();
builder.Services.AddTransient<DesktopPlayPodcastViewModel>();
builder.Services.AddTransient<DesktopLivePage>();
builder.Services.AddTransient<DesktopLiveViewModel>();
builder.Services.AddSingleton<TabletPodcastPage>();
builder.Services.AddSingleton<TabletPodcastViewModel>();
builder.Services.AddTransient<TabletShowPage>();
builder.Services.AddTransient<TabletShowViewModel>();
builder.Services.AddTransient<TabletPlayPodcastPage>();
builder.Services.AddTransient<TabletPlayPodcastViewModel>();
builder.Services.AddTransient<TabletLivePage>();
builder.Services.AddTransient<TabletLiveViewModel>();
builder.Services.AddTransient<LivePage>();
builder.Services.AddTransient<LiveViewModel>();
builder.Services.AddSingleton<TwitService>();
builder.Services.AddSingleton<FeedService>();
builder.Services.AddSingleton<Position>();
builder.Services.AddSingleton<PlaybackService>();
builder.Services.AddSingleton<PositionServices>();
builder.Services.AddSingleton<PositionDataBase>();
builder.Services.AddSingleton<FileService>();
return builder.Build();
}
}
That example above works in my program. It is an example for you to work from. It is not complete. It is missing all the things like model, data, etc. This is just so you have an idea for where to go from here.
Related
I have a view model:
public delegate void NotifyWithValidationMessages(Dictionary<string, string?> validationDictionary);
public partial class BaseViewModel : ObservableValidator
{
public event NotifyWithValidationMessages? ValidationCompleted;
public virtual ICommand ValidateCommand => new RelayCommand(() => ValidateModel());
private ValidationContext validationContext;
public BaseViewModel()
{
validationContext = new ValidationContext(this);
}
[IndexerName("ErrorDictionary")]
public ValidationStatus this[string propertyName]
{
get
{
ClearErrors();
ValidateAllProperties();
var errors = this.GetErrors()
.ToDictionary(k => k.MemberNames.First(), v => v.ErrorMessage) ?? new Dictionary<string, string?>();
var hasErrors = errors.TryGetValue(propertyName, out var error);
return new ValidationStatus(hasErrors, error ?? string.Empty);
}
}
private void ValidateModel()
{
ClearErrors();
ValidateAllProperties();
var validationMessages = this.GetErrors()
.ToDictionary(k => k.MemberNames.First().ToLower(), v => v.ErrorMessage);
ValidationCompleted?.Invoke(validationMessages);
}
}
public partial class LoginModel : BaseViewModel
{
protected string email;
protected string password;
[Required]
[DataType(DataType.EmailAddress)]
public string Email
{
get => this.email;
set
{
SetProperty(ref this.email, value, true);
ClearErrors();
ValidateAllProperties();
OnPropertyChanged("ErrorDictionary[Email]");
}
}
[Required]
[DataType(DataType.Password)]
public string Password
{
get => this.password;
set
{
SetProperty(ref this.password, value, true);
ClearErrors();
ValidateAllProperties();
OnPropertyChanged("ErrorDictionary[Password]");
}
}
}
public partial class LoginViewModel : LoginModel
{
private readonly ISecurityClient securityClient;
public LoginViewModel(ISecurityClient securityClient) : base()
{
this.securityClient = securityClient;
}
public ICommand LoginCommand => new RelayCommand(async() => await LoginAsync());
public ICommand NavigateToRegisterPageCommand => new RelayCommand(async () => await Shell.Current.GoToAsync(PageRoutes.RegisterPage, true));
private async Task LoginAsync()
{
if (this?.HasErrors ?? true)
return;
var requestParam = this.ConvertTo<LoginModel>();
var response = await securityClient.LoginAsync(requestParam);
if (response is null)
{
await Application.Current.MainPage.DisplayAlert("", "Login faild, or unauthorized", "OK");
StorageService.Secure.Remove(StorageKeys.Secure.JWT);
return;
}
await StorageService.Secure.SaveAsync<JWTokenModel>(StorageKeys.Secure.JWT, response);
await Shell.Current.GoToAsync(PageRoutes.HomePage, true);
}
}
The view looks like this:
<?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:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
xmlns:local="clr-namespace:Backend.Models;assembly=Backend.Models"
xmlns:vm="clr-namespace:MauiUI.ViewModels"
x:Class="MauiUI.Pages.LoginPage"
x:DataType="vm:LoginViewModel"
Shell.NavBarIsVisible="False">
<ScrollView>
<VerticalStackLayout Spacing="25" Padding="20,0"
VerticalOptions="Center">
<VerticalStackLayout>
<Label Text="Welcome to Amazons of Vollyeball" FontSize="28" TextColor="Gray" HorizontalTextAlignment="Center" />
</VerticalStackLayout>
<Image Source="volleyball.png"
HeightRequest="250"
WidthRequest="250"
HorizontalOptions="Center" />
<StackLayout Orientation="Horizontal">
<Frame ZIndex="1" HasShadow="True" BorderColor="White"
HeightRequest="55" WidthRequest="55" CornerRadius="25"
Margin="0,0,-32,0">
<Image Source="email.png" HeightRequest="30" WidthRequest="30" />
</Frame>
<Frame HasShadow="True" Padding="0" BorderColor="White" HeightRequest="55" HorizontalOptions="FillAndExpand">
<Entry x:Name="email" Margin="35,0,20,0" VerticalOptions="Center" Placeholder="email" Keyboard="Email"
Text="{Binding Email, Mode=TwoWay}"
toolkit:SetFocusOnEntryCompletedBehavior.NextElement="{x:Reference password}"
ReturnType="Next">
<Entry.Behaviors>
<toolkit:EventToCommandBehavior
EventName="TextChanged"
Command="{Binding ValidateCommand}" />
</Entry.Behaviors>
</Entry>
</Frame>
</StackLayout>
<Label x:Name="lblValidationErrorEmail" Text="{Binding [Email].Error}" TextColor="Red" />
<StackLayout Orientation="Horizontal">
<Frame ZIndex="1" HasShadow="True" BorderColor="White"
HeightRequest="55" WidthRequest="55" CornerRadius="25"
Margin="0,0,-32,0">
<Image Source="password.jpg" HeightRequest="30" WidthRequest="30"/>
</Frame>
<Frame HasShadow="True" Padding="0" BorderColor="White" HeightRequest="55" HorizontalOptions="FillAndExpand">
<Entry x:Name="password" Margin="35,0,20,0" VerticalOptions="Center" Placeholder="password" IsPassword="True"
Text="{Binding Password, Mode=TwoWay}">
<Entry.Behaviors>
<toolkit:EventToCommandBehavior
EventName="TextChanged"
Command="{Binding ValidateCommand}" />
</Entry.Behaviors>
</Entry>
</Frame>
</StackLayout>
<Label x:Name="lblValidationErrorPassword" Text="{Binding [Password].Error}" TextColor="Red" />
<Button Text="Login" WidthRequest="120" CornerRadius="25" HorizontalOptions="Center" BackgroundColor="Blue"
Command="{Binding LoginCommand}" />
<StackLayout Orientation="Horizontal" Spacing="5" HorizontalOptions="Center">
<Label Text="Don't have an account?" TextColor="Gray"/>
<Label>
<Label.FormattedText>
<FormattedString>
<Span Text="Register" TextColor="Blue">
<Span.GestureRecognizers>
<TapGestureRecognizer Command="{Binding NavigateToRegisterPageCommand}" />
</Span.GestureRecognizers>
</Span>
</FormattedString>
</Label.FormattedText>
</Label>
</StackLayout>
</VerticalStackLayout>
</ScrollView>
</ContentPage>
public partial class LoginPage : ContentPage
{
private RegisterViewModel viewModel => BindingContext as RegisterViewModel;
public LoginPage(LoginViewModel viewModel)
{
InitializeComponent();
viewModel.ValidationCompleted += OnValidationHandler;
BindingContext = viewModel;
#if ANDROID
MauiUI.Platforms.Android.KeyboardHelper.HideKeyboard();
#elif IOS
MauiUI.Platforms.iOS.KeyboardHelper.HideKeyboard();
#endif
}
private void OnValidationHandler(Dictionary<string, string> validationMessages)
{
if (validationMessages is null)
return;
lblValidationErrorEmail.Text = validationMessages.GetValueOrDefault("email");
lblValidationErrorPassword.Text = validationMessages.GetValueOrDefault("password");
}
}
When the
public ValidationStatus this[string propertyName] or the ValidateModel() triggers in BaseViewModel, the this.GetErrors() form the ObservableValidator class, return no errors, even if there are validation errors.
Interesting part was that, when I did not use MVVM aproach, and used LoginModel that inherited the BaseViewModel, then worked.
I am out of idea.
thnx
I do not write my own properties. Instead, I let MVVM handle it.
Lets say we have this:
public partial class MainViewModel : BaseViewModel
{
[Required(ErrorMessage = "Text is Required Field!")]
[MinLength(5, ErrorMessage = "Text length is minimum 5!")]
[MaxLength(10, ErrorMessage = "Text length is maximum 10!")]
[ObservableProperty]
string _text = "Hello";
Where BaseViewModel is inheriting ObservableValidator.
Now I can use Validation command:
[RelayCommand]
void Validate()
{
ValidateAllProperties();
if (HasErrors)
Error = string.Join(Environment.NewLine, GetErrors().Select(e => e.ErrorMessage));
else
Error = String.Empty;
IsTextValid = (GetErrors().ToDictionary(k => k.MemberNames.First(), v => v.ErrorMessage) ?? new Dictionary<string, string?>()).TryGetValue(nameof(Text), out var error);
}
Or use partial method:
partial void OnTextChanged(String text)
{
ValidateAllProperties();
if (HasErrors)
Error = string.Join(Environment.NewLine, GetErrors().Select(e => e.ErrorMessage));
else
Error = String.Empty;
IsTextValid = (GetErrors().ToDictionary(k => k.MemberNames.First(), v => v.ErrorMessage) ?? new Dictionary<string, string?>()).TryGetValue(nameof(Text), out var error);
}
Where Error is:
[ObservableProperty]
string _error;
And IsTextValid is:
[ObservableProperty]
bool _isTextValid;
Now you can bind those properties to whatever you want to display the error, or indicate that there is an error with your Text.
This is a working example, using validation, CommunityToolkit.MVVM and BaseViewModel class.
I made a demo based on your code and make some debugs. The thing i found is that you just clear the Errors ClearErrors();. Then you could not get any message.
Workaround:
In LoginModel the Email setter, put ClearErrors(); before SetProperty(ref this.email, value, true);.
[DataType(DataType.EmailAddress)]
public string Email
{
get => this.email;
set
{
ClearErrors();
SetProperty(ref this.email, value, true);
....
}
}
In BaseViewModel, comment out other ClearErrors(); in Indexer and ValidateModel() since you have already cleared it.
[IndexerName("ErrorDictionary")]
public ValidationStatus this[string propertyName]
{
get
{
//ClearErrors();
ValidateAllProperties();
...
}
}
private void ValidateModel()
{
//ClearErrors();
ValidateAllProperties();
....
}
However, your code show two ways of invoking the ValidateAllProperty() :
First is because in the .xaml, you set Text="{Binding Email, Mode=TwoWay}". That means when changing the text, the setter of Email in your LoginModel will fire and so raise propertyChanged for the Indexer.
Second is EventToCommandBehavior set in the .xaml. This also invoke
ValidateAllProperty(). And pass the text to label Text which has already binded Text="{Binding [Email].Error}".
From my point of view, one is enough. You have to decide which one to use. Better not mix these two methods together that may cause troubles.
Also for MVVM structure, you could refer to Model-View-ViewModel (MVVM).
Hope it works for you.
I cannot get the expected behaviour of SelectedIndex to work. The Item is not shown. The ItemSource, ItemDisplayBinding and SelectedItem are working when the picker is selected, But when the view is first displayed the Pickers are not showing the objects from the List that they are bound to.
I have created a test .Maui APP as follows;
The View MainPage.xml:
<?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:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
xmlns:models="clr-namespace:MauiPicker;assembly=MauiPicker"
xmlns:viewModels="clr-namespace:MauiPicker"
x:Class="MauiPicker.MainPage"
x:DataType="viewModels:MainViewModel">
<Grid
ColumnDefinitions="*"
RowDefinitions="*,*">
<CollectionView
Grid.Row="0"
Grid.Column="0"
ItemsSource="{Binding PartAResultLists}"
SelectionMode="None">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:PartAResultList">
<Grid Padding="5">
<Border>
<Grid Padding="10"
ColumnDefinitions="Auto,*"
RowDefinitions="Auto"
RowSpacing="7">
<Label Text="Outlet Type:"
Grid.Column="0" Grid.Row="0"
HorizontalOptions="End"
VerticalOptions="Center"
Margin="0,0,0,0"/>
<Border
Grid.Column="1"
Grid.Row="0"
Grid.ColumnSpan="2">
<Picker
Title="Select an Outlet"
ItemsSource="{Binding Source={RelativeSource AncestorType={x:Type viewModels:MainViewModel}}, Path=Outlets}"
ItemDisplayBinding="{Binding Name}"
SelectedIndex="{Binding OutletIndex}"
SelectedItem="{Binding OutletName}">
</Picker>
</Border>
</Grid>
</Border>
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<Button
Grid.Row="1"
Grid.Column="0"
Text="Reload List"
HorizontalOptions="Center"
VerticalOptions="Center"
Command="{Binding Source={RelativeSource AncestorType={x:Type viewModels:MainViewModel}}, Path=LoadResultsCommand}">
</Button>
</Grid>
</ContentPage>
The code behind MainPage.xml.cs
namespace MauiPicker;
public partial class MainPage : ContentPage
{
public MainPage(MainViewModel vm)
{
InitializeComponent();
BindingContext = vm;
}
}
The ViewModel MainViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MvvmHelpers;
namespace MauiPicker
{
public partial class MainViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject
{
public MainViewModel()
{
LoadResults();
}
[RelayCommand]
async Task LoadResults()
{
Outlets = new ObservableRangeCollection<Outlet>
{
new Outlet(){Name="Outlet0"},
new Outlet(){Name="Outlet1"},
new Outlet(){Name="Outlet2"},
};
PartAResultLists = new ObservableRangeCollection<PartAResultList>
{
new PartAResultList(){OutletIndex = 0, OutletName= new Outlet(){Name="Outlet0" } },
new PartAResultList(){OutletIndex=1, OutletName= new Outlet(){Name="Outlet1" }},
new PartAResultList(){OutletIndex = 2, OutletName= new Outlet(){Name="Outlet2" }},
new PartAResultList(){OutletIndex = 0, OutletName= new Outlet(){Name="Outlet0" }},
new PartAResultList(){OutletIndex = 2, OutletName= new Outlet(){Name="Outlet2" }}
};
}
[ObservableProperty]
ObservableRangeCollection<Outlet> outlets;
[ObservableProperty]
ObservableRangeCollection<PartAResultList> partAResultLists;
}
}
The models;
using CommunityToolkit.Mvvm.ComponentModel;
namespace MauiPicker
{
public partial class Outlet : ObservableObject
{
[ObservableProperty]
public string name;
}
}
using CommunityToolkit.Mvvm.ComponentModel;
namespace MauiPicker
{
public partial class PartAResultList : ObservableObject
{
[ObservableProperty]
public Outlet outletName;
[ObservableProperty]
public int outletIndex;
}
}
MauiProgram.cs
using CommunityToolkit.Maui;
using Microsoft.Extensions.Logging;
namespace MauiPicker;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseMauiCommunityToolkit()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
builder.Services.AddSingleton<MainPage>();
builder.Services.AddSingleton<MainViewModel>();
#if DEBUG
builder.Logging.AddDebug();
#endif
return builder.Build();
}
}
First I want to point out, for anyone who is reading this, that the code in the question is bound correctly.
Second, I want to say that new Outlet(Name="1") and new Outlet(Name="1") are not equal. Just because some property has the same value, does not mean that the objects will be the same. They are saved at two different places, and they are not the same. Not unless you make sure that only "Name" matters, when checking if they are equal.
Third, If you check the community toolkit MVVM autogenerated property code, you will see something like:
if(!global::System.Collections.Generic.EqualityComparer<string>.Default.Equals(name, value))
And calling OnChanged in that IF block. This is optimization to help only waste resources, when something has to be re-rendered.
This is why, when you set:
new PartAResultList(){OutletIndex = 0...
And then if you call, for example:
PartAResultLists[0].OutletIndex = 0;
The picker will remain blank. But if you call this:
PartAResultLists[0].OutletIndex = 1;
Surprisingly it will change.
(Again, giving example with the index, because the object will not work for the reasons I pointed out earlier).
There is no need to bind to Object and to Index at the same time. In your situation this can only lead to more bugs.
Your bindings are setup well. You have to fix your code around them here and there.
Edit: You do not have to actually change the value. Just trying to explain the problem that OnPropertyChanged("OutletIndex") is not called.
I have a simple project using Community Toolkit Mvvm tool and a collectionview. The issue is that the SelectionChangeCommand of the CollectionView doesn´t fired when I select an element of the collection. I created this project because on another more complex project the error is that it can't find the binding command on the viewmodel. I know that the connection between the view and viewmodel is working because the collectionview is filling with elements on the viewmodel and also I am able to change the visibility of the border through the binded property.
Sample project: https://github.com/luis95gr/MvvmError.App
Code
LoginPage (first page with collectionview)
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MvvmError.Views.LoginPage"
xmlns:models="clr-namespace:MvvmError.Models"
Title="LoginPage">
<RefreshView VerticalOptions="FillAndExpand">
<Border BackgroundColor="Red" IsVisible="{Binding Visible}" Stroke="Red" HorizontalOptions="FillAndExpand" Padding="0,0" Margin="0,0,0,0" VerticalOptions="FillAndExpand">
<Border.StrokeShape>
<RoundRectangle CornerRadius="40,0,0,0" />
</Border.StrokeShape>
<CollectionView Background="Transparent" ItemsSource="{Binding Messages}" SelectionMode="Single" SelectedItem="{Binding MessageSelected}" SelectionChangedCommand="{Binding MessageSelectedCommand}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:MessagesMessages">
<Grid RowDefinitions="100*" ColumnDefinitions="100*">
<Label Grid.Row="0" Text="{Binding Asunto}" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Border>
</RefreshView>
LoginPageViewModel
public partial class LoginPageViewModel : ObservableObject
{
[ObservableProperty]
private MessagesMessages messageSelected;
[ObservableProperty]
private bool visible;
public ObservableCollection<MessagesMessages> Messages { get; } = new();
public LoginPageViewModel()
{
Visible = true;
GetMessages();
}
[RelayCommand]
async void MessageSelectedCommand()
{
var snackbar = Snackbar.Make("Hola");
await snackbar.Show();
}
private void GetMessages()
{
Messages.Add(new MessagesMessages
{
Asunto = "Hola"
});
Messages.Add(new MessagesMessages
{
Asunto = "Hola1"
});
Messages.Add(new MessagesMessages
{
Asunto = "Hola2"
});
Messages.Add(new MessagesMessages
{
Asunto = "Hola3"
});
}
}
LoginPage.xaml.cs
public partial class LoginPage : ContentPage
{
public LoginPage(LoginPageViewModel loginPageViewModel)
{
InitializeComponent();
BindingContext= loginPageViewModel;
}
}
MauiProgram.cs
builder.Services.AddTransient<LoginPage>();
builder.Services.AddSingleton<ViewModels.LoginPageViewModel>();
you are not following the naming convention
The name of the generated command will be created based on the method
name. The generator will use the method name and append "Command" at
the end, and it will strip the "On" prefix, if present. Additionally,
for asynchronous methods, the "Async" suffix is also stripped before
"Command" is appeneded.
to generate a command named MessageSelectedCommand your method should look like
[RelayCommand]
async void MessageSelected()
I'm developing a UWP app and I'm facing a problem. The app uses the MVVM pattern with Template10. I have created a similar solution that recreates the problem that I'm facing. In that solution, a list of orders are displayed, the user chooses an order and then click the "Edit" button. Then a second page is displayed and pre-loaded with the previous selected order, in this second page the user can edit the order. The problem is in the second page, the data bound to comboboxes doesn't show. Maybe the problem is related to this question. In my case, the SelectedValue is set before the ItemsSource. After debugging, I have reached these lines of code in OrderEditionPage.g.cs:
private void Update_ViewModel(global::ComboApp.ViewModels.OrderEditionPageViewModel obj, int phase)
{
this.bindingsTracking.UpdateChildListeners_ViewModel(obj);
if (obj != null)
{
if ((phase & (NOT_PHASED | DATA_CHANGED | (1 << 0))) != 0)
{
this.Update_ViewModel_SelectedOrder(obj.SelectedOrder, phase);
}
if ((phase & (NOT_PHASED | (1 << 0))) != 0)
{
this.Update_ViewModel_BusinessAssociates(obj.BusinessAssociates, phase);
this.Update_ViewModel_TransactionTypes(obj.TransactionTypes, phase);
this.Update_ViewModel_OrderTypes(obj.OrderTypes, phase);
this.Update_ViewModel_ShowSelectedOrder(obj.ShowSelectedOrder, phase);
}
}
}
If I could achieve this line of code be executed at last, my problem would be solved: this.Update_ViewModel_SelectedOrder(obj.SelectedOrder, phase);
How could I achieve this? How does Visual Studio determine the order of this lines?
OrderEditionPage.xaml
<Page
x:Class="ComboApp.Views.OrderEditionPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:myconverters="using:ComboApp.Converters"
xmlns:t10converters="using:Template10.Converters"
mc:Ignorable="d">
<Page.Resources>
<t10converters:ChangeTypeConverter x:Key="TypeConverter" />
<myconverters:DateTimeConverter x:Key="DateTimeConverter" />
</Page.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel
Padding="15, 5"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<TextBox
Header="Order #"
Margin="5"
Width="150"
HorizontalAlignment="Left"
Text="{x:Bind ViewModel.SelectedOrder.ExternalId, Mode=TwoWay}" />
<ComboBox
Header="Business Associate"
Margin="5"
MinWidth="300"
SelectedValuePath="BusinessAssociateId"
DisplayMemberPath="Name1"
ItemsSource="{x:Bind ViewModel.BusinessAssociates}"
SelectedValue="{x:Bind ViewModel.SelectedOrder.BusinessAssociateId, Mode=TwoWay, Converter={StaticResource TypeConverter}}" />
<DatePicker
Header="Delivery Date"
Margin="5"
MinWidth="0"
Width="200"
Date="{x:Bind ViewModel.SelectedOrder.DeliveryDate, Mode=TwoWay, Converter={StaticResource DateTimeConverter}}" />
<ComboBox
Header="Transaction"
MinWidth="200"
Margin="5"
SelectedValuePath="Value"
DisplayMemberPath="Display"
ItemsSource="{x:Bind ViewModel.TransactionTypes}"
SelectedValue="{x:Bind ViewModel.SelectedOrder.TransactionType, Mode=TwoWay}" />
<TextBox
Header="Priority"
Margin="5"
MaxWidth="150"
HorizontalAlignment="Left"
Text="{x:Bind ViewModel.SelectedOrder.Priority}" />
<ComboBox
Header="Type"
Margin="5"
MinWidth="200"
SelectedValuePath="Value"
DisplayMemberPath="Display"
ItemsSource="{x:Bind ViewModel.OrderTypes}"
SelectedValue="{x:Bind ViewModel.SelectedOrder.OrderType, Mode=TwoWay}" />
<TextBox
Header="Information"
Margin="5"
Height="100"
AcceptsReturn="True"
TextWrapping="Wrap"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Text="{x:Bind ViewModel.SelectedOrder.Information, Mode=TwoWay}" />
<Button
Margin="5"
Content="Show"
Width="100"
HorizontalAlignment="Right"
Command="{x:Bind ViewModel.ShowSelectedOrder}" />
</StackPanel>
</ScrollViewer>
</Page>
OrderEditionPage.xaml.cs
using ComboApp.ViewModels;
using Windows.UI.Xaml.Controls;
namespace ComboApp.Views
{
public sealed partial class OrderEditionPage : Page
{
public OrderEditionPageViewModel ViewModel => DataContext as OrderEditionPageViewModel;
public OrderEditionPage()
{
this.InitializeComponent();
}
}
}
OrderEditionPageViewModel.cs
using ComboApp.Models;
using ComboApp.Services;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using Template10.Mvvm;
using Template10.Utils;
using Windows.UI.Xaml.Navigation;
namespace ComboApp.ViewModels
{
public class OrderEditionPageViewModel
: ViewModelBase
{
private IBusinessAssociateService businessAssociateService;
private Order selectedOrder;
public Order SelectedOrder
{
get { return selectedOrder; }
set { Set(ref selectedOrder, value); }
}
public ObservableCollection<object> TransactionTypes { get; set; } = new ObservableCollection<object>();
public ObservableCollection<object> OrderTypes { get; set; } = new ObservableCollection<object>();
public ObservableCollection<BusinessAssociate> BusinessAssociates { get; set; } = new ObservableCollection<BusinessAssociate>();
public OrderEditionPageViewModel(IBusinessAssociateService businessAssociateService)
{
this.businessAssociateService = businessAssociateService;
TransactionTypes.Add(new { Value = "I", Display = "Incoming" });
TransactionTypes.Add(new { Value = "O", Display = "Outgoing" });
TransactionTypes.Add(new { Value = "T", Display = "Transfer" });
OrderTypes.Add(new { Value = "M", Display = "Manual" });
OrderTypes.Add(new { Value = "A", Display = "Automatic" });
OrderTypes.Add(new { Value = "S", Display = "Semi-automatic" });
}
public override async Task OnNavigatedToAsync(object parameter, NavigationMode mode, IDictionary<string, object> state)
{
// Loading buiness associates
var response = await businessAssociateService.GetNextPageAsync();
if (response.IsSuccessful)
{
BusinessAssociates.AddRange(response.Result.Items);
}
SelectedOrder = (Order)parameter;
await base.OnNavigatedToAsync(parameter, mode, state);
}
private DelegateCommand showSelectedOrder;
public DelegateCommand ShowSelectedOrder => showSelectedOrder ?? (showSelectedOrder = new DelegateCommand(async () =>
{
await Views.MessageBox.ShowAsync(JsonConvert.SerializeObject(SelectedOrder, Formatting.Indented));
}));
}
}
It is a known issue of x:Bind when the SelectedValue of a ComboBox is sometimes set before its ItemsSource, you can read more about it here.
As a workaround you can use Bindings instead of x:Bind, but make sure that ItemsSource binding is placed before SelectedValue binding in XAML.
Alternatively you can try calling Bindings.Update() in the Page_Loaded event of your second page.
In my main page, i have a listview of all the items, and once user clicks on one of them, it will navigate to a detail page.
In the detail page, i create a last button to jump to last item,
<Button Content="Last" HorizontalAlignment="Center" >
<Interactivity:Interaction.Behaviors>
<Interactions:EventTriggerBehavior EventName="Click">
<Interactions:InvokeCommandAction Command="{x:Bind Path=ViewModel.LastCommand, Mode=OneWay}"/>
</Interactions:EventTriggerBehavior>
</Interactivity:Interaction.Behaviors>
</Button>
and here is part of my viewmodel for the page
class DetailPageViewModel : ViewModelBase
{
private MyItem item;
public MyItem Item
{
get { return item; }
set { SetProperty(ref item, value); }
}
public DetailPageViewModel()
{
LastCommand = new DelegateCommand(LastItemExecute, CanLastItemExecute);
LastCommand.ObservesProperty(() => Item);
}
private DelegateCommand lastCommand;
public DelegateCommand LastCommand
{
get { return lastCommand; }
set { SetProperty(ref lastCommand, value); }
}
private bool CanLastItemExecute()
{
if (Item.Index!= 1)
{
return true;
}
else
{
return false;
}
}
private void LastItemExecute()
{
Item= _context.Items.Single(p => p.Index== Item.Index- 1);
}
}
Everything works fine here, except that if i click on the first item in the listview, or jump from second item, the last button will not be disabled, click on it won't do anything though.
But i would like to disable the button if the detail page is showing the first item, any help plz?
But i would like to disable the button if the detail page is showing the first item.
Your "Last Button" is in the DetailPage, I don't know how you bind data to controls in this DetaiPage, but if you are using Mater/Detail pattern, you can refer to the official Master/detail sample, in the ItemViewModel model, there is a property Item_ID. My suggestion is that you can also add a ID properity into your data model for ListView in the Master page, so can the ID be passed within the selected Item to detail page after navigation.
Then in the DetailPageViewModel, you can get this ID and bind to the Button's IsEnable property with Converter:
<Page.Resources>
<local:ButtonEnableConverter x:Key="cvt" />
</Page.Resources>
...
<Button VerticalAlignment="Center" HorizontalAlignment="Center" Content="Last Item" Grid.Row="3"
IsEnabled="{x:Bind Item.ID, Mode=OneWay, Converter={StaticResource cvt}}">
<Interactivity:Interaction.Behaviors>
<Core:EventTriggerBehavior EventName="Click">
<Core:InvokeCommandAction Command="{Binding Path=LastCommand, Mode=OneWay}" />
</Core:EventTriggerBehavior>
</Interactivity:Interaction.Behaviors>
</Button>
The code of ButtonEnableConverter is like this:
public class ButtonEnableConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
var itemID = (int)value;
if (itemID != 0)
return true;
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
Update:
I didn't wrote a full sample to test it, but it should be something like this:
<Button Content="Last Item" Grid.Row="1" HorizontalAlignment="Center" >
<Interactivity:Interaction.Behaviors>
<Core:DataTriggerBehavior Binding="{Binding Item.ID, Mode=OneWay}" ComparisonCondition="NotEqual" Value="100">
<Core:ChangePropertyAction PropertyName="IsEnabled" Value="True"/>
</Core:DataTriggerBehavior>
<Core:DataTriggerBehavior Binding="{Binding Item.ID, Mode=OneWay}" ComparisonCondition="Equal" Value="100">
<Core:ChangePropertyAction PropertyName="IsEnabled" Value="False"/>
</Core:DataTriggerBehavior>
<Core:EventTriggerBehavior EventName="Click">
<Core:InvokeCommandAction Command="{Binding Path=LastCommand, Mode=OneWay}" />
</Core:EventTriggerBehavior>
</Interactivity:Interaction.Behaviors>
</Button>
You can have a try.