After navigating back button command is not responding to click - maui

I am trying to learn .NET Maui by building a simple app like the android Contacts app. I have a main page that has a list of items with a floating button at the bottom to add a new item as shown in the xaml below. When the user clicks on the imagebutton I navigate to a CreatePage that allows the user to enter the values for each field in an Account object. I am using the CommunityToolkit.MVVM library to handle the MVVM stuff.
In MainPageViewModel I navigate to the CreatePage using the "Shell.Current.GoToAsync(route)" in the CreateNewAccount method shown in the MainPageViewModel:
This works fine for the first time I navigate to add a new Account. When I navigate back to the MainPage and try to click the imagebutton again to add another Account the button is non-responsive. I do not see why it is not handling the button click when I navigated back. Any thoughts?
This is part of the XAML in MainPage.xaml
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<CollectionView Grid.Row="0"
Background="Transparent"
IsGrouped="False"
ItemSizingStrategy="MeasureAllItems"
ItemsLayout="VerticalList"
ItemsSource="{Binding Accounts}"
SelectedItem="{Binding SelectedAccount, Mode=TwoWay}"
SelectionMode="Single">
<CollectionView.EmptyView>
<StackLayout Padding="12">
<Label HorizontalOptions="Center" Text="No Accounts" />
</StackLayout>
</CollectionView.EmptyView>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="m:Account">
<StackLayout Orientation="Horizontal" Padding="10">
<Label Text="{Binding AccountName}"
FontSize="Large"/>
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<ImageButton
Grid.Row="0"
Command="{Binding CreateCommand}"
Source="add_box_black_48dp.svg"
Background="Transparent"
HorizontalOptions="End"
VerticalOptions="End"/>
</Grid>
public class MainPageViewModel : ObservableObject
{
private IDataContext _context;
private bool _isBusy;
private Account _selectedAccount;
public ObservableCollection<Account> Accounts { get; private set; }
public AsyncRelayCommand RefreshCommand { get; private set; }
public AsyncRelayCommand CreateCommand { get; private set; }
public bool IsBusy { get=>_isBusy; set => SetProperty(ref _isBusy, value); }
public Account SelectedAccount { get=>_selectedAccount; set => SetProperty(ref _selectedAccount, value); }
public MainPageViewModel(IDataContext context)
{
_context = context;
Accounts = new ObservableCollection<Account>();
RefreshCommand = new AsyncRelayCommand(Refresh);
CreateCommand = new AsyncRelayCommand(CreateNewAccount);
Accounts.Add(new Account { Id = 1, AccountName = "a1", UserName = "a2", Password = "a3" });
}
async Task CreateNewAccount()
{
var route = $"{nameof(CreatePage)}";
await Shell.Current.GoToAsync(route);
}
internal async Task InitializeAsync()
{
await Refresh();
}
async Task Refresh()
{
IsBusy = true;
var accounts = await _context.GetAllAsync();
if (Accounts.Count > 0)
{
Accounts.Clear();
}
foreach (var item in accounts)
{
Accounts.Add(item);
}
IsBusy = false;
}
}
In CreatePageViewModel I create an AsyncRelayCommand as follows and navigate back to the mainpage after the Account is added using "Shell.Current.GoToAsync("..")"
public AsyncRelayCommand SaveCommand { get; private set; }
public CreatePageViewModel(IDataContext dataContext)
{
_dataContext = dataContext;
NewAccount = new Account();
SaveCommand = new AsyncRelayCommand(AddAccount);
}
private async Task AddAccount()
{
var accountFound = await _dataContext.FindByName(NewAccount.AccountName);
if (accountFound is not null)
{
await Application.Current.MainPage.DisplayAlert("Alert", "Account alreaady exists", "OK");
return;
}
await _dataContext.InsertAccountAsync(NewAccount);
await Shell.Current.GoToAsync("..");
}

why you await all events?
async Task CreateNewAccount()
{
var route = $"{nameof(CreatePage)}";
// we won't wait - await Shell.Current.GoToAsync(route);
_ = Shell.Current.GoToAsync(route);
}
and everything will work

Related

NET MAUI CommunityToolkit.Mvvm not validating

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.

Bind ProgressRing to mvvm

I am downloading data from an api, and displaying that data in the view. As I wait I want to display a ProgressRing, but when I bind it dosen't work
Bind the ring to the cityData property, with a two way mode and update it when the property changes
Created a new property in the VM, that is true by default and it will turn false when I get the data back
XAML
<TextBox x:Name="currentLocation"
PlaceholderText="Please wait..."
IsReadOnly="True"
Margin="20"
Width="300"
Text="{Binding cityData, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<ListView RelativePanel.Below="currentLocation"
x:Name="ForecastList"
Margin="20"
SelectedItem="{Binding currentDay, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding dailyForecasts}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Margin="10">
<TextBlock x:Name="dateTB" Text="{Binding Date.DayOfWeek}" />
<TextBlock x:Name="highTB" Text="{Binding Temperature.Maximum.Value, Converter={StaticResource cv}}" FontSize="10" />
<TextBlock x:Name="lowTB" FontSize="10" Text="{Binding Temperature.Minimum.Value, Converter={StaticResource cv}}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ProgressRing x:Name="pRing" RelativePanel.Above="ForecastList" RelativePanel.AlignHorizontalCenterWith="currentLocation" IsActive="{Binding ring, Mode=TwoWay}" RelativePanel.Below="currentLocation" />
VM
public class WeatherVM: INotifyPropertyChanged
{
public AccuWeather accuWeather { get; set; }
private string _cityData;
public string cityData
{
get { return _cityData; }
set
{
if (value != _cityData)
{
_cityData = value;
onPropertyChanged("cityData");
GetWeatherData();
}
}
}
private DailyForecast _currentDay;
public DailyForecast currentDay
{
get { return _currentDay; }
set
{
if (value != _currentDay) \
{
_currentDay = value;
onPropertyChanged("currentDay");
}
}
}
public bool ring { get; set; } = true;
public ObservableCollection<DailyForecast> dailyForecasts { get; set; }
public WeatherVM()
{
GetCuurentLocation();
dailyForecasts = new ObservableCollection<DailyForecast>();
}
private async void GetCuurentLocation() {
cityData = await BingLocator.GetCityData();
}
public async void GetWeatherData() {
var geoposition = await LocationManager.GetGeopositionAsync();
var currentLocationKey = await WeatherAPI.GetCityDstaAsync(geoposition.Coordinate.Point.Position.Latitude, geoposition.Coordinate.Point.Position.Longitude);
var weatherData = await WeatherAPI.GetWeatherAsync(currentLocationKey.Key);
if (weatherData != null) {
foreach (var item in weatherData.DailyForecasts) {
dailyForecasts.Add(item);
}
}
currentDay = dailyForecasts[0];
ring = false;
}
public event PropertyChangedEventHandler PropertyChanged;
private void onPropertyChanged(string property) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
}
}
The progress ring appears when the app launch, and disappear when the data is returned
From your code, it seems to be a layout issue. First you put ListView below currentLocation, then set ProgressRing above ListView and below currentLocation, so the height of ProgressRing wil be zero. I'm not clear about your layout, you can try to only set RelativePanel.Above="ForecastList" for ProgressRing to see if it will appear.
You can use Windows Community Toolkit control for showing progress ring (Busy indicator).
<Page ...
xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls"/>
<controls:Loading x:Name="LoadingControl" IsLoading="{Binding IsBusy}">
<!-- Loading screen content -->
</controls:Loading>

binding city to an AutoSuggestBox

I am trying to learn MVVVM, by doing a weather application, using a Udemy course as a reference, the Bing Maps Api, and the OpenWeather API.
I am trying to bind the city with the text of the AutoSuggestBox of my xaml
I have done the WeatherVM and bind it to the view, as a page resource
I also did a quick Method that gets me the City and the Country code, where I am (Using the Bing Maps api)
I Called the method from the MainPage.cs, only to see if it works, and it work fine I get the City and Country Code as expected
WeatherVM
public OpenWeather OpenWeather { get; set; }
private Task<string> _city;
public Task<string> city {
get { return _city; }
set {
_city = value;
GetLocationData();
}
}
public WeatherVM() {
OpenWeather = new OpenWeather();
}
private async void GetLocationData() {
var cityData = await MapLocator.GetCityData();
}
}
}
MainPage.xaml
x:Class="MVVM_Example.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MVVM_Example"
xmlns:vm="using:MVVM_Example.ViewModel"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Page.Resources>
<vm:WeatherVM x:Key="vm" />
</Page.Resources>
<Grid DataContext="{StaticResource vm}">
<AutoSuggestBox Margin="40" QueryIcon="Find"
PlaceholderText="Search"
Text="{Binding Source={StaticResource vm}, Path=city, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
</Page>
I expect "Orlando" to appear in my AutoSuggestBox
When you use Binding, you need to set the DataContext of the page to be an instance of your binding source class instead of set the page.Resource. For more details, you can refer to this document.
<Page.DataContext>
<local:WeatherVM x:Name="viewModelInDataContext"/>
</Page.DataContext>
<Grid>
<AutoSuggestBox Margin="40" QueryIcon="Find"
PlaceholderText="Search"
Text="{Binding Path=city, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
In code-behind, suppose you need to assign a value to the city.
this.viewModelInDataContext.city = "Orlando";
Update:
The ViewModel you showed has some issues. First, you need to assign a value to the cityData property, if not, the text of AutoSuggestBox which bound with the cityData will always display empty. Second, you need to implement INotifyChanged in WeatherVM. When your cityData changes, the text of the AutoSuggestBox will change. Third, do not set the property type to asynchronous like Task, this will affect the display, it's better to set string cityData.
public class WeatherVM : INotifyPropertyChanged
{
public WeatherVM(){
GetData();
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private string _cityData;
public string cityData
{
get
{
return _cityData;
}
set
{
_cityData = value;
OnPropertyChanged();
}
}
private async void GetData()
{
cityData = await MapLocator.GetCityData();
}
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}

Master/Details binding MVVM

I have some problem with correct binding my folder with images to master details and other operation with them.
So, I have model of folder and image
public class AppFolder
{
private long id;
private List<AppImage> appImages;
public AppFolder() { }
public List<AppImage> AppImages { get => appImages; set => appImages = value; }
public long Id { get => id; set => id = value; }
}
public class AppImage
{
private int id;
private string title;
private ImageSource appImageURL;
public AppImage() { }
public AppImage(string title, ImageSource imageSource)
{
Title = title;
AppImageURL = imageSource;
}
public int Id { get => id; set => id = value; }
public string Title { get => title; set => title = value; }
public ImageSource AppImageURL { get => appImageURL; set => appImageURL = value; }
}
And I bind List to Master/Details.
public class UserPhotosViewModel : ViewModelBase
{
private readonly IDataService dataService;
private readonly INavigationService navigationService;
public UserPhotosViewModel(IDataService dataService, INavigationService navigationService)
{
this.dataService = dataService;
this.navigationService = navigationService;
Initialize();
}
private async Task Initialize()
{
var item = new List<AppFolder>();
try
{
item = await dataService.GetDataList();
FolderList = item;
}
catch (Exception ex)
{
}
}
private List<AppFolder> folderList;
public List<AppFolder> FolderList
{
get { return folderList; }
set
{
folderList = value;
RaisePropertyChanged(nameof(FolderList));
}
}
}
Example xaml file
<controls:MasterDetailsView ItemsSource="{Binding FolderList}">
<controls:MasterDetailsView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Id}"></TextBlock>
</DataTemplate>
</controls:MasterDetailsView.ItemTemplate>
<controls:MasterDetailsView.DetailsTemplate>
<DataTemplate>
<controls:AdaptiveGridView ItemsSource="{Binding AppImages}"
OneRowModeEnabled="False"
ItemHeight="205"
DesiredWidth="205"
SelectionMode="Multiple"
Margin="0 6 0 0"
>
<controls:AdaptiveGridView.ItemTemplate>
<DataTemplate >
<Grid Background="White" Margin="10">
<Image
Source="{Binding AppImageURL}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="Uniform"
/>
<TextBlock Text="{Binding TextTitle}"></TextBlock>
</Grid>
</DataTemplate>
</controls:AdaptiveGridView.ItemTemplate>
</controls:AdaptiveGridView>
</DataTemplate>
</controls:MasterDetailsView.DetailsTemplate>
</controls:MasterDetailsView>
So, it's work correct and I saw my folders with images on page
enter image description here
Look nice and I think it all.
But when I want to add event and SelectedItem to AdaptiveGridView from MVVM model, I saw that it doesn't see them. Visual Studio show me that I could to write them in Model "AppFolder" but it's nonsens....
So, my question: How I can add event (binding command/method) to adaptive grid from UserPhotosViewModel?
Thank you for your time.
UPDATE
enter image description here
2) User double click on image and program send folder with this image and other to page and binding them to FlipView (imitation full screen viewer)
I try to add, but that way also offers to write event and property in model. I use this way for settings in navigation panel (parent control for master/details)
You just need to add command property in your AppFolder class like the following:
public class AppFolder:ViewModelBase
{
private long id;
private List<AppImage> appImages;
public AppFolder() { }
public List<AppImage> AppImages { get => appImages; set => appImages = value; }
public long Id { get => id; set => id = value; }
public RelayCommand<object> relayCommand { get; set; }
}
Then, in your UserPhotosViewModel, you could declare a method for initializing this command.
Since I do not know what the dataService.GetDataList() is. I just change this place in your code and make a simple code sample for you.
private void Initialize()
{
var item = new List<AppFolder>();
try
{
List<AppImage> ls = new List<AppImage>();
ls.Add(new AppImage() { Id = 1, Title = "aaa", AppImageURL = new BitmapImage(new Uri("ms-appx:///Assets/1.jpg")) });
ls.Add(new AppImage() { Id = 2, Title = "bbb", AppImageURL = new BitmapImage(new Uri("ms-appx:///Assets/2.jpg")) });
item.Add(new AppFolder() { Id = 1, AppImages = ls,relayCommand=new RelayCommand<object>(DoubleTapCommand) });
FolderList = item;
}
catch (Exception ex)
{
}
}
private void DoubleTapCommand(object obj)
{
//the obj will be an AppFolder object
}
<controls:MasterDetailsView.DetailsTemplate>
<DataTemplate>
<controls:AdaptiveGridView x:Name="grid" ItemsSource="{Binding AppImages}"
OneRowModeEnabled="False"
ItemHeight="205"
DesiredWidth="205"
SelectionMode="Multiple"
Margin="0 6 0 0">
<Interactivity:Interaction.Behaviors>
<Interactions:EventTriggerBehavior EventName="DoubleTapped">
<Interactions:InvokeCommandAction Command="{Binding relayCommand}" CommandParameter="{Binding}"></Interactions:InvokeCommandAction>
</Interactions:EventTriggerBehavior>
</Interactivity:Interaction.Behaviors>
<controls:AdaptiveGridView.ItemTemplate>
<DataTemplate >
<Grid Background="White" Margin="10">
<Image
Source="{Binding AppImageURL}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="Uniform"
/>
<TextBlock Text="{Binding TextTitle}"></TextBlock>
</Grid>
</DataTemplate>
</controls:AdaptiveGridView.ItemTemplate>
</controls:AdaptiveGridView>
</DataTemplate>
</controls:MasterDetailsView.DetailsTemplate>
1) User select N images and delete them (example)
About this requirement, I suggested that you'd better add a button to bind command for deleting selected items.

UWP - How to specify the order of updates when use x:Bind?

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.