How to customize collectionview in dotnetmaui - maui

I am porting my Xamarin Forms application to dotnet maui and I want to customize the collection view for android so that the items will stack from the end. I have done it in Xamarin Forms as follows,
public class ChatCollectionViewRenderer : CollectionViewRenderer
{
public ChatCollectionViewRenderer(Context context) : base(context)
{
this.SetItemViewCacheSize(20);
this.HasFixedSize = true;
}
protected override LayoutManager SelectLayoutManager(IItemsLayout layoutSpecification)
{
var manager = new LinearLayoutManager(Context, LinearLayoutManager.Vertical, false);
manager.StackFromEnd = true;
return manager;
}
}
How to do it in maui using handlers?

Found it, it can be done like this,
Microsoft.Maui.Controls.Handlers.Items.CollectionViewHandler.Mapper.AppendToMapping("ChatStackFromEnd", (h, v) =>
{
var recycleView = h.PlatformView;
var manager = new AndroidX.RecyclerView.Widget.LinearLayoutManager(recycleView.Context, AndroidX.RecyclerView.Widget.LinearLayoutManager.Vertical, false);
manager.StackFromEnd = true;
recycleView.SetLayoutManager(manager);
});
Trying it out...

Related

Create WinUI3/MVVM Most Recently Used (MRU) List in Menu Bar

I would like to create a classic "Recent Files" list in my Windows app menu bar (similar to Visual Studio's menu bar -> File -> Recent Files -> see recent files list)
The MRU list (List < string > myMRUList...) is known and is not in focus of this question. The problem is how to display and bind/interact with the list according to the MVVM rules.
Microsoft.Toolkit.Uwp.UI.Controls's Menu class will be removed in a future release and they recommend to use MenuBar control from the WinUI. I haven't found any examples, that use WinUI's MenuBar to create a "Recent Files" list.
I'm using Template Studio to create a WinUI 3 app. In the ShellPage.xaml I added
<MenuFlyoutSubItem x:Name="mruFlyout" Text="Recent Files"></MenuFlyoutSubItem>
and in ShellPage.xaml.c
private void Button_Click(object sender, RoutedEventArgs e)
{
mruFlyout.Items.Insert(mruFlyout.Items.Count, new MenuFlyoutItem(){ Text = "C:\\Test1_" + DateTime.Now.ToString("MMMM dd") } );
mruFlyout.Items.Insert(mruFlyout.Items.Count, new MenuFlyoutItem(){ Text = "C:\\Test2_" + DateTime.Now.ToString("MMMM dd") } );
mruFlyout.Items.Insert(mruFlyout.Items.Count, new MenuFlyoutItem(){ Text = "C:\\Test3_" + DateTime.Now.ToString("MMMM dd") } );
}
knowing this is not MVVM, but even this approach does not work properly, because the dynamically generated MenuFlyoutItem can be updated only once by Button_Click() event.
Could anybody give me an example, how to create the "Recent Files" functionality, but any help would be great! Thanks
Unfortunately, it seems that there is no better solution than handling this in code behind since the Items collection is readonly and also doesn't response to changes in the UI Layout.
In addition to that, note that because of https://github.com/microsoft/microsoft-ui-xaml/issues/7797, updating the Items collection does not get reflected until the Flyout has been closed and reopened.
So assuming your ViewModel has an ObservableCollection, I would probably do this:
// 1. Register collection changed
MyViewModel.RecentFiles.CollectionChanged += RecentFilesChanged;
// 2. Handle collection change
private void RecentFilesChanged(object sender, NotifyCollectionChangedEventArgs args)
{
// 3. Create new UI collection
var flyoutItems = list.Select(entry =>
new MenuFlyoutItem()
{
Text = entry.Name
}
);
// 4. Updating your MenuFlyoutItem
mruFlyout.Items.Clear();
flyoutItems.ForEach(entry => mruFlyout.Items.Add(entry));
}
Based on chingucoding's answer I got to the "recent files list" binding working.
For completeness I post the detailed code snippets here (keep in mind, that I'm not an expert):
Again using Template Studio to create a WinUI 3 app.
ShellViewModel.cs
// constructor
public ShellViewModel(INavigationService navigationService, ILocalSettingsService localSettingsService)
{
...
MRUUpdateItems();
}
ShellViewModel_RecentFiles.cs ( <-- partial class )
using System.Collections.ObjectModel;
using System.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Windows.Storage;
using Windows.Storage.AccessCache;
using Windows.Storage.Pickers;
namespace App_MostRecentUsedTest.ViewModels;
public partial class ShellViewModel : ObservableRecipient
{
public ObservableCollection<MRUItem> MRUItems{ get; set;} = new();
// update ObservableCollection<MRUItem>MRUItems from MostRecentlyUsedList
public void MRUUpdateItems()
{
var mruTokenList = StorageApplicationPermissions.MostRecentlyUsedList.Entries.Select(entry => entry.Token).ToList();
var mruMetadataList = StorageApplicationPermissions.MostRecentlyUsedList.Entries.Select(entry => entry.Metadata).ToList(); // contains path as string
MRUItems.Clear(); var i = 0;
foreach (var path in mruMetadataList)
{
MRUItems.Add(new MRUItem() { Path = path, Token = mruTokenList[i++] });
}
}
// called if user selects a recent used file from menu bar list
[RelayCommand]
protected async Task MRULoadFileClicked(int? fileId)
{
if (fileId is not null)
{
var mruItem = MRUItems[(int)fileId];
FileInfo fInfo = new FileInfo(mruItem.Path ?? "");
if (fInfo.Exists)
{
StorageFile? file = await Windows.Storage.AccessCache.StorageApplicationPermissions.MostRecentlyUsedList.GetFileAsync(mruItem.Token);
if (file is not null)
{
Windows.Storage.AccessCache.StorageApplicationPermissions.MostRecentlyUsedList.Add(file, file.Path); // store file.Path into Metadata
MRUUpdateItems();
// LOAD_FILE(file);
}
}
else
{
}
}
await Task.CompletedTask;
}
[RelayCommand]
protected async Task MenuLoadFileClicked()
{
StorageFile? file = await GetFilePathAsync();
if (file is not null)
{
Windows.Storage.AccessCache.StorageApplicationPermissions.MostRecentlyUsedList.Add(file, file.Path); // store file.Path into Metadata
MRUUpdateItems();
// LOAD_FILE(file);
}
await Task.CompletedTask;
}
// get file path with filePicker
private async Task<StorageFile?> GetFilePathAsync()
{
FileOpenPicker filePicker = new();
filePicker.FileTypeFilter.Add(".txt");
IntPtr hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow);
WinRT.Interop.InitializeWithWindow.Initialize(filePicker, hwnd);
return await filePicker.PickSingleFileAsync();
}
public class MRUItem : INotifyPropertyChanged
{
private string? path;
private string? token;
public string? Path
{
get => path;
set
{
path = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(path));
}
}
public string? Token
{
get => token;
set => token = value;
}
public event PropertyChangedEventHandler? PropertyChanged;
}
}
ShellPage.xaml
<MenuBar>
<MenuBarItem x:Name="ShellMenuBarItem_File">
<MenuFlyoutItem x:Uid="ShellMenuItem_File_Load" Command="{x:Bind ViewModel.MenuLoadFileClickedCommand}" />
<MenuFlyoutSubItem x:Name="MRUFlyout" Text="Recent Files..." />
</MenuBarItem>
</MenuBar>
ShellPage.xaml.cs
// constructor
public ShellPage(ShellViewModel viewModel)
{
...
// MRU initialziation
// assign RecentFilesChanged() to CollectionChanged-event
ViewModel.MRUItems.CollectionChanged += RecentFilesChanged;
// Add (and RemoveAt) trigger RecentFilesChanged-event to update MenuFlyoutItems
ViewModel.MRUItems.Add(new MRUItem() { Path = "", Token = ""});
ViewModel.MRUItems.RemoveAt(ViewModel.MRUItems.Count - 1);
}
// MRU Handle collection change
private void RecentFilesChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
// project each MRUItems list element into a new UI MenuFlyoutItem flyoutItems list
var i = 0;
var flyoutItems = ViewModel.MRUItems.Select(entry =>
new MenuFlyoutItem()
{
Text = " " + i.ToString() + " " + FilenameHelper.EllipsisString(entry.Path, 65),
Command = ViewModel.MRULoadFileClickedCommand,
CommandParameter = i++
}
);
//// If you want to update the list while it is shown,
//// you will need to create a new FlyoutItem because of
//// https://github.com/microsoft/microsoft-ui-xaml/issues/7797
// Create a new flyout and populate it
var newFlyout = new MenuFlyoutSubItem();
newFlyout.Text = MRUFlyout.Text; // Text="Recent Files...";
// Updating your MenuFlyoutItem
flyoutItems.ToList().ForEach(item => newFlyout.Items.Add(item));
// Get index of old sub item and remove it
var oldIndex = ShellMenuBarItem_File.Items.IndexOf(MRUFlyout);
ShellMenuBarItem_File.Items.Remove(MRUFlyout);
// Insert the new flyout at the correct position
ShellMenuBarItem_File.Items.Insert(oldIndex, newFlyout);
// Assign newFlyout to "old"-MRUFlyout
MRUFlyout = newFlyout;
}

How do I properly use DI with IHttpClientFactory in .NET MAUI

I haven't found anything about HttpClient in .NET MAUI.
Does anyone know if the service:
builder.Services.AddHttpClient<IMyService, MyService>();
is possible in MAUI's startup MauiProgram.cs? And then inject HttpClient to where it's going to be used. I have tried everything and it does not seem to work. Only AddSingleton of HttpClient works for me, but it doesn't seem optimal.
PS.: I had to install nuget package Microsoft.Extensions.Http in order to use the AddHttpClient service.
UPDATES:
WORKING CODE:
MauiProgram.cs
builder.Services.AddTransient<Service<Display>, DisplayService>();
builder.Services.AddTransient<Service<Video>, VideoService>();
builder.Services.AddTransient<Service<Image>, ImageService>();
builder.Services.AddTransient<Service<Log>, LogService>();
builder.Services.AddSingleton(sp => new HttpClient() { BaseAddress = new Uri("https://api.myapi.com") });
Example of VideosViewModel.cs using a service
[INotifyPropertyChanged]
public partial class VideosViewModel
{
readonly Service<Video> videoService;
[ObservableProperty]
ObservableCollection<Video> videos;
[ObservableProperty]
bool isEmpty;
[ObservableProperty]
bool isRefreshing;
public VideosViewModel(Service<Video> videoService)
{
this.videoService = videoService;
}
[ICommand]
internal async Task LoadVideosAsync()
{
#if ANDROID || IOS || tvOS || Tizen
UserDialogs.Instance.ShowLoading("Henter videoer fra databasen...");
#endif
await Task.Delay(2000);
Videos = new();
try
{
await foreach (Video video in videoService.GetAllAsync().OrderBy(x => x.Id))
{
Videos.Add(video);
}
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
finally
{
IsRefreshing = false;
#if ANDROID || IOS || tvOS
UserDialogs.Instance.HideLoading();
#endif
if (Videos.Count is 0)
{
IsEmpty = true;
}
else
{
IsEmpty = false;
}
}
}
[ICommand]
async Task UploadVideoAsync()
{
await Shell.Current.DisplayAlert("Upload en video", "Under opbygning - kommer senere!", "OK");
}
}
NOT WORKING CODE:
MauiProgram.cs
builder.Services.AddHttpClient<Service<Display>, DisplayService>(sp => sp.BaseAddress = new Uri("https://api.myapi.com"));
builder.Services.AddHttpClient<Service<Video>, VideoService>(sp => sp.BaseAddress = new Uri("https://api.myapi.com"));
builder.Services.AddHttpClient<Service<Image>, ImageService>(sp => sp.BaseAddress = new Uri("https://api.myapi.com"));
builder.Services.AddHttpClient<Service<Log>, LogService>(sp => sp.BaseAddress = new Uri("https://api.myapi.com"));
VideosViewModel.cs
Same as above working code.
What specifically doesn't work is that I get object reference exception on OrderBy(x => x.Id), specifically highlighted x.Id in ViewModel. Removing OrderBy method gives no longer exceptions, but the view shows no data except one random empty Frame.
Do not use builder.Services.AddHttpClient in MAUI.
Use one instance.

Customize the Xamarin.Forms Picker Popup List

I know how to create a custom renderer to customize the actual text of the Xamarin forms picker, but how do you customize, say, the background color or text of the list that pops up when you click on the picker text box?
You can refer to the following code :
in iOS
using System;
using Xamarin.Forms;
using xxx;
using xxx.iOS;
using UIKit;
using Xamarin.Forms.Platform.iOS;
using Foundation;
[assembly:ExportRenderer(typeof(MyPicker), typeof(MyiOSPicker))]
namespace xxx.iOS
{
public class MyiOSPicker:PickerRenderer,IUIPickerViewDelegate
{
IElementController ElementController => Element as IElementController;
public MyiOSPicker()
{
}
[Export("pickerView:viewForRow:forComponent:reusingView:")]
public UIView GetView(UIPickerView pickerView, nint row, nint component, UIView view)
{
UILabel label = new UILabel
{
//here you can set the style of item!!!
TextColor = UIColor.Blue,
Text = Element.Items[(int)row].ToString(),
TextAlignment = UITextAlignment.Center,
};
return label;
}
protected override void OnElementChanged(ElementChangedEventArgs<Picker> e)
{
base.OnElementChanged(e);
if(Control!=null)
{
UIPickerView pickerView = (UIPickerView)Control.InputView;
pickerView.WeakDelegate = this;
pickerView.BackgroundColor = UIColor.Yellow; //set the background color of pickerview
}
}
}
}
in Android
using System;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using xxx;
using xxx.Droid;
using Android.Widget;
using Android.App;
using System.Linq;
[assembly: ExportRenderer(typeof(MyPicker), typeof(MyAndroidPicker))]
namespace xxx.Droid
{
public class MyAndroidPicker:PickerRenderer
{
IElementController ElementController => Element as IElementController;
public MyAndroidPicker()
{
}
private AlertDialog _dialog;
protected override void OnElementChanged(ElementChangedEventArgs<Picker> e)
{
base.OnElementChanged(e);
if (e.NewElement == null || e.OldElement != null)
return;
Control.Click += Control_Click;
}
protected override void Dispose(bool disposing)
{
Control.Click -= Control_Click;
base.Dispose(disposing);
}
private void Control_Click(object sender, EventArgs e)
{
Picker model = Element;
var picker = new NumberPicker(Context);
if (model.Items != null && model.Items.Any())
{
// set style here
picker.MaxValue = model.Items.Count - 1;
picker.MinValue = 0;
picker.SetBackgroundColor(Android.Graphics.Color.Yellow);
picker.SetDisplayedValues(model.Items.ToArray());
picker.WrapSelectorWheel = false;
picker.Value = model.SelectedIndex;
}
var layout = new LinearLayout(Context) { Orientation = Orientation.Vertical };
layout.AddView(picker);
ElementController.SetValueFromRenderer(VisualElement.IsFocusedProperty, true);
var builder = new AlertDialog.Builder(Context);
builder.SetView(layout);
builder.SetTitle(model.Title ?? "");
builder.SetNegativeButton("Cancel ", (s, a) =>
{
ElementController.SetValueFromRenderer(VisualElement.IsFocusedProperty, false);
// It is possible for the Content of the Page to be changed when Focus is changed.
// In this case, we'll lose our Control.
Control?.ClearFocus();
_dialog = null;
});
builder.SetPositiveButton("Ok ", (s, a) =>
{
ElementController.SetValueFromRenderer(Picker.SelectedIndexProperty, picker.Value);
// It is possible for the Content of the Page to be changed on SelectedIndexChanged.
// In this case, the Element & Control will no longer exist.
if (Element != null)
{
if (model.Items.Count > 0 && Element.SelectedIndex >= 0)
Control.Text = model.Items[Element.SelectedIndex];
ElementController.SetValueFromRenderer(VisualElement.IsFocusedProperty, false);
// It is also possible for the Content of the Page to be changed when Focus is changed.
// In this case, we'll lose our Control.
Control?.ClearFocus();
}
_dialog = null;
});
_dialog = builder.Create();
_dialog.DismissEvent += (ssender, args) =>
{
ElementController?.SetValueFromRenderer(VisualElement.IsFocusedProperty, false);
};
_dialog.Show();
}
}
}

How to find framework element inside viewmodel using Prism

I previously used Caliburn.Micro for my projects before universal windows application. Now I'm porting my apps to universal windows and decided to use Prism Library. Because there are lots of uwp sample for that. But I'm too beginner and don't know how to convert my old viewmodels.
I'm using webview to show some generated html. In caliburn I can find webview in viewmodel using OnViewLoaded event;
protected override void OnViewLoaded(object view)
{
base.OnViewLoaded(view);
var frameworkElement = view as FrameworkElement;
if (frameworkElement == null)
throw new ArgumentException();
var browser = frameworkElement.FindName("browser") as WebView;
if (browser == null)
throw new ArgumentException();
_webBrowser = browser;
}
But I didn't find any event can provide this. In prism there are only OnNavigatedTo and OnNavigatingFrom events.
Do prism have workaround for that?
Sorry about late reply,
I aggree with Igor and Tseng that will break MVVM pattern. But I'm trying to do something complex. Maybe there is a way of doing with it MVVM but I don't want to lose too much time on this.
What I found solution for the problem is as following. I wrote a VisualHelper
public class VisualHelper
{
public static T FindVisualChildInsideFrame<T>(DependencyObject depObj) where T : DependencyObject
{
var frame = FindVisualChild<Frame>(depObj);
if (frame != null && frame.Content is Page)
return FindVisualChild<T>(frame.Content as Page);
return null;
}
public static T FindVisualChild<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj != null)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
if (child != null && child is T)
{
return (T)child;
}
T childItem = FindVisualChild<T>(child);
if (childItem != null)
return childItem;
}
}
return null;
}
}
In my viewmodel, using following code finds WebView control. I'm using SplitView control thats why I'm using FindVisualChildInsideFrame method.
public override void OnNavigatedTo(NavigatedToEventArgs e, Dictionary<string, object> viewModelState)
{
base.OnNavigatedTo(e, viewModelState);
var browser = VisualHelper.FindVisualChildInsideFrame<WebView>(Window.Current.Content);
}

Testing code in a custom NancyFx Bootstrapper

I have a custom Nancy Bootstrapper which uses StructureMapNancyBootstrapper but the issue is the same regardless of container.
public class CustomNancyBootstrapper : StructureMapNancyBootstrapper
{
protected override void RequestStartup(IContainer container, IPipelines pipelines, NancyContext context)
{
var auth = container.GetInstance<ICustomAuth>();
auth.Authenticate(context);
}
}
I want to write a test to assert that Authenticate is called with the context... something like this...
[Test]
public void RequestStartup_Calls_CustomAuth_Authenticate_WithContext()
{
// set up
var mockAuthentication = new Mock<ICustomAuth>();
var mockContainer = new Mock<IContainer>();
var mockPipelines = new Mock<IPipelines>();
var context = new NancyContext();
mockContainer.Setup(x => x.GetInstance<ICustomAuth>()).Returns(mockAuthentication.Object);
// exercise
_bootstrapper.RequestStartup(_mockContainer.Object, _mockPipelines.Object, context);
// verify
mockAuthentication.Verify(x => x.Authenticate(context), Times.Once);
}
The problem is that I can't call RequestStartup because it's protected as defined in NancyBootstrapperBase.
protected virtual void RequestStartup(TContainer container, IPipelines pipelines, NancyContext context);
Is there a "proper"/"offical" Nancy way to do this without creating another derived class and exposing the methods as that just seems like a hack?
Thanks
I guess you can "fake" the request by using Browser from Nancy.Testing:
var browser = new Browser(new CustomNancyBootstrapper());
var response = browser.Get("/whatever");
There is a good set of articles about testing NancyFx application:
http://www.marcusoft.net/2013/01/NancyTesting1.html
Turns out Nancy offers a IRequetStartup interface so you can take the code out of the custom bootstrapper and do something like this...
public class MyRequestStart : IRequestStartup
{
private readonly ICustomAuth _customAuthentication;
public MyRequestStart(ICustomAuth customAuthentication)
{
if (customAuthentication == null)
{
throw new ArgumentNullException(nameof(customAuthentication));
}
_customAuthentication = customAuthentication;
}
public void Initialize(IPipelines pipelines, NancyContext context)
{
_customAuthentication.Authenticate(context);
}
}
and the test is easy and concise
[Test]
public void When_Initialize_Calls_CustomAuth_Authenticate_WithContext()
{
// set up
var mockAuth = new Mock<ICustomAuth>();
var requestStartup = new MyRequestStart(mockAuth.Object);
var mockPipeline = new Mock<IPipelines>();
var context = new NancyContext();
// exercise
requestStartup.Initialize(mockPipeline.Object, context);
// verify
mockAuth.Verify(x => x.Authenticate(context), Times.Once);
}
https://github.com/NancyFx/Nancy/wiki/The-Application-Before%2C-After-and-OnError-pipelines#implementing-interfaces