ObservableValidator CustomValidation validates too early - .net-maui

I have a .Net MAUI app that uses CommunityToolkit.Mvvm. The viewmodel is validated using ObservableValidator with custom validation:
public partial class BasePageModel : ObservableValidator
...
public partial class DistributorsLocatorPageModel : BasePageModel
{
...
[ObservableProperty]
[CustomValidation(typeof(DistributorsLocatorPageModel), nameof(ValidateProductLines))]
private List<ProductLineListItem> _productLines;
...
public static ValidationResult ValidateProductLines(List<ProductLineListItem> productLines, ValidationContext context)
{
bool isValid = productLines.Where(pl => pl.Selected).Any();
if (isValid)
{
return ValidationResult.Success;
}
return new("Please Choose Product Line!");
}
Here is XAML:
<StackLayout BindableLayout.ItemsSource="{Binding ProductLines}" >
<BindableLayout.ItemTemplate>
<DataTemplate x:DataType="viewmodel:ProductLineListItem">
<Grid
ColumnDefinitions="*,auto"
Margin="0,0,0,10">
<StackLayout VerticalOptions="CenterAndExpand" Grid.Column="0">
<Label Text="{Binding Name}" />
</StackLayout>
<Switch IsToggled="{Binding Selected}" Style="{StaticResource SwitchStyle}" Grid.Column="1" />
</Grid>
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
For some reason, this validation fires without any call to ValidateAllProperties(), as soon as the view is loaded, thus always showing the error message "Please Choose Product Line!" before the user has chance to choose anything. What am I missing?
P.S. Regular (non-custom) validation behaves as expected.

Related

Unable to use FlexLayout in a BindableLayout with MAUI

I'm trying to use a FlexLayout in the DataTemplate section of a BindableLayout with MAUI. I'm using a StackLayout as a BindableLayout. However, the app does not even start because of an error that seems to come from the Xaml code. Everything is fine when I use a StackLayout or a Grid instead but I want to use a FlexLayout if possible. What am I doing wrong?
Here is a picture of the error:
XAML Error
Exception details 1
Exception details 2
The error message is "Élément introuvable" which means Unable to find element.
Here is the xaml code:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:tests="clr-namespace:Tests"
xmlns:components="clr-namespace:Tests.Components"
Shell.NavBarIsVisible="False"
x:Class="Tests.MainPage">
<StackLayout>
<StackLayout x:Name="Items">
<BindableLayout.ItemTemplate>
<DataTemplate x:DataType="tests:Phone">
<FlexLayout>
<Image MaximumHeightRequest="150" Source="{Binding Image}"/>
<Label Text="{Binding Name}"/>
<Label Text="{Binding Price}"/>
<Label Text="{Binding Year}"/>
</FlexLayout>
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
</StackLayout>
</ContentPage>
Here is the .cs code:
public partial class MainPage : ContentPage {
private ObservableCollection<Phone> phones = new();
public MainPage() {
InitializeComponent();
InitPhoneCollection();
}
private void InitPhoneCollection() {
Phone phone1 = new Phone {
Name = "iPhone",
Price = 100,
Year = "2019",
Image = "iphone.jpg"
};
Phone phone2 = new Phone {
Name = "Samsung",
Price = 150,
Year = "2021",
Image = "samsung.jpg"
};
Phone phone3 = new Phone {
Name = "Blackberry",
Price = 200,
Year = "2022",
Image = "blackberry.jpg"
};
phones.Add(phone1);
phones.Add(phone2);
phones.Add(phone3);
BindableLayout.SetItemsSource(Items, phones);
}
}
Thanks
I tried a StackLayout and a Grid instead but for best results but I would prefer a FlexLayout.
This is a known issue that being tracked in the links below, you can follow up there.
https://github.com/dotnet/maui/issues/9905
https://github.com/dotnet/maui/issues/11909
https://github.com/dotnet/maui/issues/9075
As an alternative workaround, you can use StackLayout or Grid instead of FlexLayout like below:
<StackLayout x:Name="Items">
<BindableLayout.ItemTemplate>
<DataTemplate x:DataType="tests:Phone">
<StackLayout>
<Image MaximumHeightRequest="150" Source="{Binding Image}"/>
<Label Text="{Binding Name}"/>
<Label Text="{Binding Price}"/>
<Label Text="{Binding Year}"/>
</StackLayout>
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>

Login detail across all pages of xamarin portable forms

How can i display Login detail of user across all pages in xamarin portable project(iOS and Andriod). I am not able to implement MasterPage concept as we use to do in ASP.net or Frame in Silverlight.
I have already gone through MasterDetailPage and I think it doesn't fulfill my requirement.
Create a static variable UserName in your App.cs file.
public class App : Application
{
public static bool IsUserLoggedIn { get; set; }
public static string UserName { get; set; }
public App()
{
if (!IsUserLoggedIn)
{
MainPage = new NavigationPage(new LoginPage());
}
else
{
MainPage = new NavigationPage(new MenuPage());
}
}
}
On LoginPage.cs file, store the username in static variable of App.cs file when user login for the first time. I am assuming that you have login page with two entry fields such as username and password field along with a login button. So when user click on login button store the username from entry field as :
App.UserName = usernameEntry.Text.ToString();
On successful login authentication you might redirect to MenuPage.
On MenuPage.cs file you can easily access the static variable of App.cs file as :
public partial class MenuPage : MasterDetailPage
{
public MenuPage()
{
InitializeComponent();
string name = App.UserName;
}
}
Likewise you can access this Static variable across any pages of your portable project. This static variable will persist till you don't remove the app from the device's current running application tray list of android/ios.
Sample project of master detail page :
https://github.com/xamarin/xamarin-forms-samples/tree/master/Navigation
Sample project of Login flow :
https://github.com/xamarin/xamarin-forms-samples/tree/master/Navigation/LoginFlow
Here is something that you want to do:
on login you get the image and name to display on the drawer header and you store in a constant.
your Drawer page here
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MasterDetailsPageLogin.Views.DrawerPage"
Title="Menu"
BackgroundColor="#8999A6"
Padding="0,20,0,0">
<StackLayout Orientation="Vertical">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="10"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="80"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="5"/>
</Grid.RowDefinitions>
<BoxView Grid.ColumnSpan="3"
Grid.RowSpan="4"
BackgroundColor="#8999A6"/>
<Image
x:Name="img"
Grid.Column="1"
Grid.Row="1"
HorizontalOptions="Start"
VerticalOptions="End"
WidthRequest="75" HeightRequest="75"/>
<Label
x:Name="myName"
Grid.Column="1"
Grid.Row="2"/>
</Grid>
<ListView x:Name="listed" SeparatorColor="Transparent">
</ListView>
</StackLayout>
</ContentPage>
Code in your drawerpage.xaml.cs
public DrawerPage()
{
InitializeComponent();
string howILook = Constants.Image;
img.Source = new UriImageSource
{
Uri = new Uri(howILook),
CachingEnabled = true,
CacheValidity = new TimeSpan(1, 0, 0, 0) //Caching image for a day
};
myName.Text = "Welcome Mr. " + Constants.Username;
}
Here is a template I have created for the login logic and master details page.
https://github.com/ak47akshaykulkarni/MasterDetailsPageLogin

Pure XAML approach to evenly distributing an unknown number of children in a container

Let's say I have a StackPanel (Orientation="Horizontal") with a fixed width and a variable/unknown amount of children, how do I tell all the children in XAML (not in code) to set their widths' to all be equal and fill up the entire StackPanel's width?
It's easy enough to do with Grid when you know the number of children (just use ColumnDefinitions with Width="*"), but I'm not sure how to do it with panels (StackPanel, PivotHeaderPanel, etc) or lists (GridView, ListView, etc).
According to your description, what you need is a panel like the UniformGrid in WPF. UniformGrid provides a way to arrange content in a grid where all the cells in the grid have the same size. However there is no build-in UniformGrid in UWP. We can implement it by ourselves or use a third party UniformGrid like what in WinRTXamlToolkit.
Here using the UniformGrid in WinRTXamlToolkit for example. To use WinRTXamlToolkit, we can install it from nuget: WinRT XAML Toolkit for Windows 10.
Then add xmlns in the page like:
xmlns:toolkit="using:WinRTXamlToolkit.Controls"
After this, we can use the UniformGrid like:
<toolkit:UniformGrid Rows="1" />
Here I set the Rows property to 1 and the Columns property to the default value 0. A value of zero (0) for the Columns property specifies that the column count is computed based on the number of rows and the number of visible child elements that are in the Grid. So all the children in this UniformGrid will be put in one row and as this is a UniformGrid, all the columns will have the same width.
Following is a sample that uses UniformGrid as the ItemsPanel of a GridView:
XAML:
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<StackPanel>
<GridView x:Name="MyGridView" Height="60">
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<toolkit:UniformGrid Rows="1" />
</ItemsPanelTemplate>
</GridView.ItemsPanel>
<GridView.ItemContainerStyle>
<Style TargetType="GridViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
</GridView.ItemContainerStyle>
<GridView.ItemTemplate>
<DataTemplate>
<StackPanel Background="Red">
<TextBlock HorizontalAlignment="Center" FontSize="30" Text="{Binding }" />
</StackPanel>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
<Button Click="Button_Click">Add a item</Button>
</StackPanel>
</Grid>
Code-behind:
public sealed partial class MainPage : Page
{
private ObservableCollection<string> myList = new ObservableCollection<string> { "1", "2", "3", "4", "5" };
private int i = 6;
public MainPage()
{
this.InitializeComponent();
MyGridView.ItemsSource = myList;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
myList.Add(i.ToString());
i += 1;
}
}
And it looks like:

How to set textwrapping in Button

did a search on internet, could not find the solution. I think I missed something for below code to make the text wrap:
<Button x:Name="btnCustomerAging" Background="Green" BorderBrush="Green" Foreground="White" FontSize="33" HorizontalAlignment="Left" Margin="662,106,0,0" Grid.Row="1" VerticalAlignment="Top" Height="213" Width="238">
<TextWrapping>
Customer Locations
</TextWrapping>
</Button>
This will work.
<Button x:Name="btnCustomerAging" Background="Green" BorderBrush="Green" Foreground="White" FontSize="33"
HorizontalAlignment="Left" Margin="662,106,0,0" Grid.Row="1" VerticalAlignment="Top" Height="213" Width="238">
<TextBlock Text="Customer Locations" TextWrapping="Wrap" />
</Button>
You can create your own WrapButton and use it in XAML like this:
<local:WrapButton x:Name="MyButton" Text="Text that will wrap"/>
This is the code for WrapButton:
public sealed class WrapButton : Button
{
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(WrapButton), new PropertyMetadata(string.Empty));
public string Text { get { return (string)GetValue(TextProperty); } set { SetValue(TextProperty, value); } }
public WrapButton()
{
var textBlock = new TextBlock { TextAlignment = TextAlignment.Center, TextWrapping = TextWrapping.Wrap };
textBlock.SetBinding(TextBlock.TextProperty, new Binding { Source = this, Path = new PropertyPath("Text") });
Content = textBlock;
}
}

Notification window in case of any operation in C: drive

I have created a sample for Notification window in silverlight4.
The issue i'm facing is that i want to show notification window whenever any operation(say file copied or deleted) is performed in my windows C: drive.
Since System.IO.FileSystemWatcher class (COM+ componenet) cannot be used in silverlight, so i found a way out using an AutomationFactory, it works like charm for me as long as i am using message box to show notification or default notification window but not in case of my own customnotification window.. :(
The code is like this -
new Thread(() =>
{
using (dynamic SWbemLocator = AutomationFactory.CreateObject("WbemScripting.SWbemLocator"))
{
SWbemLocator.Security_.ImpersonationLevel = 3;
SWbemLocator.Security_.AuthenticationLevel = 4;
dynamic IService = SWbemLocator.ConnectServer(".", #"root\cimv2");
string fileSystemWatcherQuery =
#"SELECT * FROM __InstanceOperationEvent WITHIN 3 WHERE Targetinstance ISA 'CIM_DirectoryContainsFile' and TargetInstance.GroupComponent= 'Win32_Directory.Name=""c:\\\\""'";
dynamic monitor = IService.ExecNotificationQuery(fileSystemWatcherQuery);
Dispatcher.BeginInvoke(() => MessageBox.Show(#"Now listening to file changes on c:\"));
while (true)
{
dynamic EventObject = monitor.NextEvent();
string eventType = EventObject.Path_.Class;
string path = EventObject.TargetInstance.PartComponent;
Dispatcher.BeginInvoke((Action)delegate
{
System.Windows.NotificationWindow notify = new System.Windows.NotificationWindow();
notify.Height = 74;
notify.Width = 329;
CustomNotification custom = new CustomNotification();
custom.Header = "FileSystemWatcher";
custom.Text = eventType + ":" + path;
custom.Width = notify.Width;
custom.Height = notify.Height;
custom.Closed += new EventHandler<EventArgs>(custom_Closed);
notify.Content = custom;
notify.Show(5000);
});
Dispatcher.BeginInvoke(() => MessageBox.Show(eventType + ": " + path));
}
}
}).Start();
Whenever i copied the file or delete a file from my C:, a message box appears and notification window do appears but the content is entirely blank. If i use default notification window in place of my own CustomNotification it works fine.
What i am guessing is, it has something related to threading, not sure though.. ??
CustomNotification is a class which derives from ContentConrol class and have two dependency properties namely "Header" & "Text". The control template for this i have declared in different xaml file like this -
<Style TargetType="local:CustomNotification">
<Setter.Value>
<ControlTemplate TargetType="local:CustomNotification">
<Border BorderBrush="Black" BorderThickness="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="7"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Rectangle Grid.Row="0">
<Rectangle.Fill>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFB9B9CC"/>
<GradientStop Color="#FF9191AB" Offset="1"/>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Grid Grid.Row="1">
<Grid.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFEDEDF5" Offset="0"/>
<GradientStop Color="#FFC4C3D7" Offset="1"/>
</LinearGradientBrush>
</Grid.Background>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="20"/>
</Grid.ColumnDefinitions>
<Button x:Name="closeButton" Grid.Column="2" VerticalAlignment="Top" Margin="1, 3" Width="16" Height="13">
<Button.Template>
<ControlTemplate TargetType="Button">
<Image Source="x.png"/>
</ControlTemplate>
</Button.Template>
</Button>
<StackPanel Grid.Column="1" Margin="5, 7">
<TextBlock Text="{TemplateBinding Header}" FontFamily="Verdana" FontWeight="Bold" FontSize="11"/>
<TextBlock Text="{TemplateBinding Text}" FontFamily="Verdana" FontSize="11" TextWrapping="Wrap"/>
</StackPanel>
</Grid>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Here goes my CustomNotification class code -
public class CustomNotification : ContentControl
{
public CustomNotification()
{
this.DefaultStyleKey = typeof(CustomNotification);
}
public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register(
"Header",
typeof(string),
typeof(CustomNotification),
new PropertyMetadata(OnHeaderPropertyChanged));
/// <summary>
/// Gets or sets a value that indicates whether
/// the <see cref="P:System.Windows.Controls.Label.Target" /> field is required.
/// </summary>
public string Header
{
get
{
return (string)GetValue(CustomNotification.HeaderProperty);
}
set
{
SetValue(CustomNotification.HeaderProperty, value);
}
}
private static void OnHeaderPropertyChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
}
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
"Text",
typeof(string),
typeof(CustomNotification),
new PropertyMetadata(OnTextPropertyChanged));
/// <summary>
/// Gets or sets a value that indicates whether
/// the <see cref="P:System.Windows.Controls.Label.Target" /> field is required.
/// </summary>
public string Text
{
get
{
return (string)GetValue(CustomNotification.TextProperty);
}
set
{
SetValue(CustomNotification.TextProperty, value);
}
}
private static void OnTextPropertyChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
Button closeButton = GetTemplateChild("closeButton") as Button;
if (closeButton != null)
{
closeButton.Click += new RoutedEventHandler(closeButton_Click);
}
}
public event EventHandler<EventArgs> Closed;
void closeButton_Click(object sender, RoutedEventArgs e)
{
EventHandler<EventArgs> handler = this.Closed;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
}
I assume your CustomNotification class is just a user control (i.e. inherits directly from UserControl)? Assuming so, this line:
notify.Content = tb;
is wrong. I don't know what tb is, but it should be:
notify.Content = custom;
Then you'll see the content.
Note that you have to be careful to not try and show a notification whilst another one is currently being displayed. You'll get an exception, so you'll need to keep track of this.
Hope this helps...
Chris Anderson

Resources