lundi 29 novembre 2021

UWP using ChangeView() on a ScrollViewer so that the focused item can be placed with a specific offset [C#]

The initial need was that I needed to be able to sync two listview together, however one of these listview had horizontal items and was not selectable which meant that it would not inform the first listview that the selected view had changed. 

The first listview is not only and indicator but can also allow to quickly access a certain row in our main listview, here is quick screenshot to better understand what I wanted to do, in red the two selected items that need to be sync and in the other colors that other ListView.

The main issue was to be able to detect when the user scrolled down on the second ListView Vertical and to also know at what index position he was at in the listview.

First we need to get the ScrollView of our ListView to detect when the user was scrolling.
We will create class ListViewBaseExtension.cs which will hold the following method
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 public static ScrollViewer GetScrollViewer(this DependencyObject element)
{
	if (element is ScrollViewer)
	{
		return (ScrollViewer)element;
	}

	for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
	{
		var child = VisualTreeHelper.GetChild(element, i);

		var result = GetScrollViewer(child);
		if (result == null)
		{
			continue;
		}
		else
		{
			return result;
		}
	}

	return null;
}
This method uses VisualTreeHelper which can allow us to easily access the UI visual tree.
Next, we will use the GetScrollViewer() method on our ListView to get our ScrollViewer:
//our second vertical listview
AppListView.Loaded += (sender, e) =>
{
	//getting scrollview
	ScrollViewer scrollViewer = AppListView.GetScrollViewer(); //Extension method
	if (scrollViewer != null)
	{
		scrollViewer.ViewChanging += ScrollViewerListView_ViewChanging;
	}
};
Now let's look at our method called ScrollViewerListView_ViewChanging that handles the ViewChanging event from our scrollViewer.
Everytime we will need to get the position of our items in the list using GetAllItemsPositions(), also we need to calculate the height that that user has scrolled down which will give us a currentVerticalPosition.  We will then use this to find an item that is on this position, which will then allow us to inform which item should be selected on the first listview:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private void ScrollViewerListView_ViewChanging(object sender, ScrollViewerViewChangingEventArgs e)
{
        //Offset of first listview
	double AdditionOffSetToAdd = -200;

	MyItemPositions = AppListView.GetAllItemsPositions();


	if (MyItemPositions != null)
	{
		var currentVerticalPosition = e.FinalView.VerticalOffset + AdditionOffSetToAdd;

		double itemIndex = MyItemPositions.Values.Where(a => a >= currentVerticalPosition).FirstOrDefault();

		CurrentVisibleItemIndex = MyItemPositions.FirstOrDefault(x => x.Value == itemIndex).Key;

		//Debug.WriteLine($"CurrentVisibleItemIndex :{CurrentVisibleItemIndex}");
		//Debug.WriteLine($"previousItemIndex :{previousItemIndex}");

		if (previousItemIndex != CurrentVisibleItemIndex)
		{
			//update previous
			previousItemIndex = CurrentVisibleItemIndex;

			//Debug.WriteLine("VerticalOffset :{0}", e.FinalView.VerticalOffset);
			//Debug.WriteLine("possible visible item {0}", CurrentVisibleItemIndex);

			CurrentItemIndexChangedCommand();
		}
	}
}

public event EventHandler CurrentItemIndexChanged;
private void CurrentItemIndexChangedCommand()
{
	CurrentItemIndexChanged?.Invoke(this, new EventArgs());
}
By listening to the event CurrentItemIndexChanged, we can now update our selected item on on first listview.

You can find the sample application here: https://github.com/Delaire/Samples/tree/master/SyncTwoListviews

mercredi 6 octobre 2021

Adding custom Input Validation on TextBox and PasswordBox [UWP][XAML]

We are going to go over how to create a custom TextBox & PasswordBox with a new visual state.

I needed this kind on input box for my account creation view, where the user inputs his email and his password, the default controls don't allow you to set these controls in a invalide state when you check for example if the email is valid and if the password is strong enough. 
What I wanted to do is set the border of the control to red when the user had not meet the necessary steps to go to the next step in the account creation process.

While we wait for WinUI 3.0 to support input validation here is my very simple version of how I implemented one on a TextBox and PasswordBox.

The idea was to add a property called HasError to my new control and bind it to my ViewModel, when the HasError property is changed we can use VisualStateManager to change the visual of our control. What I learned was that you can create a custom TextBox but not a PasswordBox as the PasswordBox is a sealed class.

TextBox custom control

Here is my simple control with HasError property added to a TextBox my controler is called  LoginValidatingTextBox.cs

 public class LoginValidatingTextBox : TextBox
    {
        public LoginValidatingTextBox()
        {
             this.DefaultStyleKey = typeof(LoginValidatingTextBox);
        }

        public bool HasError
        {
            get { return (bool)GetValue(HasErrorProperty); }
            set { SetValue(HasErrorProperty, value); }
        }

        public static readonly DependencyProperty HasErrorProperty =
            DependencyProperty.Register("HasError", typeof(bool), typeof(LoginValidatingTextBox), new PropertyMetadata(false, HasErrorUpdated));


        // This method will update the Validation visual state which will be defined later in the Style
        private static void HasErrorUpdated(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            LoginValidatingTextBox textBox = d as LoginValidatingTextBox;

            if (textBox != null)
            {
                if (textBox.HasError)
                    VisualStateManager.GoToState(textBox, "InvalidState", false);
                else
                    VisualStateManager.GoToState(textBox, "ValidState", false);
            }
        }
    }


Next, we need to add this VisualStateGroup to the default style of my TextBox so that when we have the InvalideState activated we can update our TextBox UI as we wish.

<VisualStateGroup x:Name="ValidationState">
 <VisualState x:Name="InvalidState">
  <Storyboard>
   <ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderElement" Storyboard.TargetProperty="BorderBrush">
    <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource DmRedBrush}" />
   </ObjectAnimationUsingKeyFrames>
  </Storyboard>
 </VisualState>
 <VisualState x:Name="ValidState">
  <Storyboard />
 </VisualState>
</VisualStateGroup>

Here is how we can implement this control in our UWP app

<controls:LoginValidatingTextBox
 x:Name="AddressTextbox"
 HasError="{Binding InvalidEmailErrorVisible}"
 PlaceholderText="Email address"
 Text="{Binding UserEmailAddress}" />

And, now when InvalidEmailErrorVisible is true we will update our TextBox as needed.

PasswordBox custom control

Next we will try to and do the same thing for the password box is not as pretty as you cant inherit from the base control you have to do it in CS of your view.

First off you need to add the same VisualStateGroup to the style of your PasswordBox same as the TextBox, int he code behind of your view you will need to listen to when your PasswordErrorVisible property has changed and call a method that will call:

VisualStateManager.GoToState(UI ELEMENT, STATE YOU WISH, false)

Here is the full code:

private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
 Debug.WriteLine(e.PropertyName);

 switch (e.PropertyName)
 {
  case "PasswordErrorVisible":
   PasswordErrorChanged();
   break;
 }
}

private void PasswordErrorChanged()
{
 if (ViewModel.PasswordErrorVisible)
  VisualStateManager.GoToState(password, "InvalidState", false);
 else
  VisualStateManager.GoToState(password, "ValidState", false);
}

and there you have it fr both TextBox and PasswordBox we now have an invalid state.

Hope this helps!
Happy coding.





jeudi 1 juillet 2021

Microsoft Login for UWP on Windows 10 apps and Xbox One using Microsoft Graph [2020] [C#]

We are going to go over how to implement Microsoft login inside your application on Windows 10 apps and Xbox One apps.


We used to use the old login endpoint login.microsoft.com to login our users and today we have update our app to use the new graph endpoint from Microsoft you can download the nuget package called Microsoft.Graph and also Microsoft.Identity.Client.


Microsoft has greatly improved how you can login to its API compared to how I used to login or it might have been me that was not doing the best way.

Next we will try to login a user using the MS documentation.  We are using simple scopes for our login "user.read" and our ClientId was registered in our Azure portal.

The login code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private async void CallMsGraphLogin()
{
	try
	{
		// Sign-in user using MSAL and obtain an access token for MS Graph
		GraphServiceClient graphClient = await SignInAndInitializeGraphServiceClient(scopes);

		// Call the /me endpoint of Graph
		Microsoft.Graph.User graphUser = await graphClient.Me.Request().GetAsync();

		string token = await SignInUserAndGetTokenUsingMSAL(scopes);
		 
		MsTokenReceived?.Invoke(token);
	}
	catch (MsalException msalEx)
	{
		MsErrorLoginReceived?.Invoke($"Error Acquiring Token:{System.Environment.NewLine}{msalEx}");
		//await DisplayMessageAsync($"Error Acquiring Token:{System.Environment.NewLine}{msalEx}");
	}
	catch (Exception ex)
	{
		MsErrorLoginReceived?.Invoke($"Error Acquiring Token Silently:{System.Environment.NewLine}{ex}");
		return;
	}
}


The app will register to the MS Graph API and will ask the user to login, which will generate the Microsoft login window.





We will not go over the methods call SignInUserAndGetTokenUsingMSAL and SignInAndInitializeGraphServiceClient as I would be saying the same thing as the Microsoft documentation which can be found here.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
private static async Task<string> SignInUserAndGetTokenUsingMSAL(string[] scopes)
{
	// Initialize the MSAL library by building a public client application
	PublicClientApp = PublicClientApplicationBuilder.Create(ClientId)
		.WithAuthority(Authority)
		.WithUseCorporateNetwork(false)
		.WithRedirectUri("https://login.microsoftonline.com/common/oauth2/nativeclient")
		 .WithLogging((level, message, containsPii) =>
		 {
			 Debug.WriteLine($"MSAL: {level} {message} ");
		 }, LogLevel.Warning, enablePiiLogging: false, enableDefaultPlatformLogging: true)
		.Build();

	// It's good practice to not do work on the UI thread, so use ConfigureAwait(false) whenever possible.
	IEnumerable<IAccount> accounts = await PublicClientApp.GetAccountsAsync().ConfigureAwait(false);
	IAccount firstAccount = accounts.FirstOrDefault();

	try
	{
		authResult = await PublicClientApp.AcquireTokenSilent(scopes, firstAccount)
										  .ExecuteAsync();
	}
	catch (MsalUiRequiredException ex)
	{
		// A MsalUiRequiredException happened on AcquireTokenSilentAsync. This indicates you need to call AcquireTokenAsync to acquire a token
		Debug.WriteLine($"MsalUiRequiredException: {ex.Message}");

		authResult = await PublicClientApp.AcquireTokenInteractive(scopes)
										  .ExecuteAsync()
										  .ConfigureAwait(false);

	}
	return authResult.AccessToken;
}

/// <summary>
/// Sign in user using MSAL and obtain a token for MS Graph
/// </summary>
/// <returns>GraphServiceClient</returns>
private async static Task<GraphServiceClient> SignInAndInitializeGraphServiceClient(string[] scopes)
{
	GraphServiceClient graphClient = new GraphServiceClient(MSGraphURL,
		new DelegateAuthenticationProvider(async (requestMessage) =>
		{
			requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", await SignInUserAndGetTokenUsingMSAL(scopes));
		}));

	return await Task.FromResult(graphClient);
}