Xamarin.Forms Listview - xamarin.forms

I'm making vocabulary app and trying to implement favorite word in my Xamarin.Forms Listview.
Please have a look below screenshots
For now i use hard code instead of MVVM
Here is my model
namespace Mamtil.Models
{
public class Word
{
public int Id { get; set; }
public string word { get; set; }
public string translation { get; set; }
public string audio { get; set; }
public bool favorite { get; set; }
public string groupBy => translation[0].ToString().ToUpper();
}
}
XAML
...
<ViewCell>
<StackLayout Orientation="Horizontal">
<Button x:Name="FavoriteButton" Image="gray_star.png" BackgroundColor="#F5F5F5" Clicked="Favorite" BorderRadius="0" WidthRequest="45" CommandParameter="{Binding .}" />
<Switch IsToggled="{Binding favorite}" WidthRequest="50"/>
<StackLayout Orientation="Vertical" HorizontalOptions="FillAndExpand" Margin="5, 0, 0, 0">
<Label Text="{Binding translation}">
<Label.FontSize>
<OnPlatform x:TypeArguments="x:Double" iOS="11" Android="18" />
</Label.FontSize>
</Label>
<Label Text="{Binding word}" TextColor="#1565C0">
<Label.FontSize>
<OnPlatform x:TypeArguments="x:Double" iOS="11" Android="13" />
</Label.FontSize>
</Label>
</StackLayout>
<Button x:Name="SpeechButton" Image="ic_mic_black_24dp.png" BackgroundColor="#F5F5F5" Clicked="GoToSpeach" BorderRadius="0" WidthRequest="45" CommandParameter="{Binding Id}" />
</StackLayout>
</ViewCell>
So the question is when i load data from database how do i initialize Image of FavoriteButton like
<!-- Here switch is toggled or not depending on Binding value. I want to do something like this but with image of Button -->
<Switch IsToggled="{Binding favorite}">
in code above it is hard coded but i want to change depending Word.favorite value. At run time i managed to change Image like this
async void Favorite(object sender, EventArgs e)
{
var b = (Button)sender;
Word t = (Word)b.CommandParameter;
//update in database
await App.MamtilRepo.UpdateFavoriteAsync(t.Id);
if (t.favorite)
b.Image = "gray_star.png";
else
b.Image = "yellow_star.png";
t.favorite = !t.favorite;
}
for sake of examle:
...
// Some thing like
if(Word.favorite)
Image = yellow_star.png;
else
Image = gray_star.png;
should i do it some where in code or in XAML
I hope could clearly describe my question. thanks in advance

You'd have to use the OnPropertyChanged() method so that it picks up the new value for the image Url, and you should also set up the binding context.
private bool _imageUrl;
public bool ImageUrl
{
get { return _imageUrl; }
set { _imageUrl = value; OnPropertyChanged(); }
}
And use BindingContext = this; in your constructor.

an old post, but here's another way to do this.
Classically one would use a converter for this.
So your button XAML would now look something like this:
<Button x:Name="FavoriteButton" Image="{Binding favorite, Converter={x:Static converter:FavoriteButton.Instance}}" BackgroundColor="#F5F5F5" Clicked="Favorite" BorderRadius="0" WidthRequest="45" CommandParameter="{Binding .}" />
you'd need to add a reference for where your converter lives in the top of your XAML file, i.e.
xmlns:converter="clr-namespace:Mamtil.Converter;assembly=Interact.Client"
With your converter looking something like this:
namespace Mamtil.Converter {
public class FavoriteButton : IValueConverter {
// this Instance property just means your converter only needs to get created once, I saw it in a Xamarin sample.
public static FavoriteButton Instance = new FavoriteButton();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
if (value == null) {
// only doing this because I've had encounters where an ImageSource has taken Exception at having null returned.
return ImageSource.FromFile("gray_star.png");
}
var fav = (bool)value;
if (fav == true) { // redundant ==, but it makes it quite explicit for any future reader glancing over your code what you intend here.
return ImageSource.FromFile("yellow_star.png");
}
return ImageSource.FromFile("gray_star.png");
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}
If you're using embedded resources rather than local files then refer to the link Rohit posted in his answer on Aug 24 '16 for how guidance on how to use those in your converter instead.

You can Bind your Image's Source to File, Uri or Resource, which sets the image to display.
FromFile - Requires a filename or filepath that can be resolved on each platform.
FromUri - Requires a Uri object, eg. new Uri("http://server.com/image.jpg") .
FromResource - Requires a resource identifier to an image file embedded in the application or PCL, with a Build Action:EmbeddedResource .
XAML :
<Image Source="{Binding MyImage}" />
ViewModel :
public ImageSource MyImage {get; set; }
You can read more about working with Images here.

DataTrigger or Converter are best solution for your problem.
<Button Image="gray_star.png">
<Button.Triggers>
<DataTrigger TargetType="Button" Binding="{Binding favorite}" Value="True">
<Setter Property="Image" Value="yellow_star.png" />
</DataTrigger>
</Button.Triggers>
</Button>

Related

Injecting sqlite database into MAUI ViewModels gives error that ViewModel does not define parameterless constructor

I am new to MAUI and I have a working project that uses an sqlite database to store my data for my project.
I am trying to inject my database access object into the ViewModel for one of my Content Pages.
I had it working previously by just creating ("new'ing up") my database access object and the database and project worked fine.
When I changed this so that I would inject my database access object into the ViewModel's constructor I get an error:
/Users/RemoteCommand/Projects/Notes/Views/AllNotesPage.xaml(9,9): Error: XLS0507: Type 'AllNotes' is not usable as an object element because it is not public or does not define a public parameterless constructor or a type converter. (Notes) IntelliSense
Here is my XAML file:
<?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:models="clr-namespace:Notes.Models"
x:Class="Notes.Views.AllNotesPage"
Title="AllNotesPage">
<ContentPage.BindingContext>
<models:AllNotes />
</ContentPage.BindingContext>
<ContentPage.ToolbarItems>
<!--<ToolbarItem Text="Add" Clicked="Add_Clicked" IconImageSource="{FontImage Glyph='+', Color=White, Size=22}"/>-->
<ToolbarItem Text="Add" Command="{Binding AddClickedCommand}" IconImageSource="{FontImage Glyph='+', Color=White, Size=22}"/>
</ContentPage.ToolbarItems>
<CollectionView x:Name="notesCollection"
ItemsSource="{Binding Notes}"
Margin="20"
SelectionMode="Single"
SelectionChanged="notesCollection_SelectionChanged">
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Vertical" ItemSpacing="10"/>
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout>
<Label Text="{Binding Text}" FontSize = "22"/>
<Label Text="{Binding Date}" FontSize="14" TextColor="Silver"/>
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ContentPage>
Here is my code behind:
namespace Notes.Views;
using Notes.Models;
using Notes.Views;
using Notes.Data;
using CommunityToolkit.Mvvm.Input;
public partial class AllNotesPage : ContentPage
{
public AllNotesPage()
{
InitializeComponent();
}
async void notesCollection_SelectionChanged(System.Object sender, Microsoft.Maui.Controls.SelectionChangedEventArgs e)
{
if(e.CurrentSelection.Count != 0)
{
var note = (Note)e.CurrentSelection[0];
await Shell.Current.GoToAsync($"{nameof(NotePage)}?{nameof(NotePage.ItemId)}={note.ID}");
}
}
}
Here is my ViewModel:
using System;
using System.Collections.ObjectModel;
using Notes.Data;
using Notes.Views;
using CommunityToolkit.Mvvm;
using CommunityToolkit.Mvvm.Input;
namespace Notes.Models;
public partial class AllNotes
{
NotesDatabase _notesDatabase;
public ObservableCollection<Note> Notes { get; set; } = new ObservableCollection<Note>();
public AllNotes(NotesDatabase notesDatabase)
{
_notesDatabase = notesDatabase;
LoadNotes();
}
[RelayCommand]
async void AddClicked()
{
await Shell.Current.GoToAsync(nameof(NotePage));
}
public async void LoadNotes()
{
Notes.Clear();
List<Note> notes = await _notesDatabase.GetItemsAsync();
foreach(Note note in notes)
{
Notes.Add(note);
}
}
}
and here is my MauiProgram where I define the dependency injection:
using Microsoft.Extensions.Logging;
using Notes.Views;
using Notes.Models;
using Notes.Data;
namespace Notes;
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");
});
#if DEBUG
builder.Logging.AddDebug();
#endif
builder.Services.AddSingleton<AllNotes>();
builder.Services.AddSingleton<AllNotesPage>();
builder.Services.AddSingleton<NotesDatabase>();
return builder.Build();
}
}
[Note: I have switched the AddSingleton to Add Transient for this page for these definitions to see if that would fix the problem but it did not]
I've tried a really basic dependency injection on an earlier test project where I injected my data access object into the code behind and I got the same error about missing a parameterless constructor and it turns out what I was missing was defining my data access object AND ContentPage as a Transient or Singleton in MauiProgram (Which is why for this project I added the data access object, ContentPage, and ViewModel as Singletons to the MauiProgram). Once I did that it worked, but now that I am using a ViewModel that I bind in the XAML I can't seem to get DI working for the ViewModel.
Please help this greenhorn!
Sincerely,
Doing this in XAML is an open issue: Resolve XAML BindingContext from ServiceCollection.
For now, do this via code behind's constructor, with a parameter:
public AllNotesPage(AllNotes vm)
{
InitializeComponent();
BindingContext = vm;
}
DI will inject vm, doing the needed instantiation of AllNotes and its NotesDatabase.

How do i make a button read the text(url) stored in the database so it runs the xamarin essentials share function

Ok so i followed a few tutorials to make it so my app can read a database file and used this https://github.com/jfversluis/ExistingSQLiteDbSample
And it did work for me, but now what I'm trying to do is so my app can use the text stored in the database so it can do a share function using Xamarin.Essentials: Share
I would prefer it was a button but no idea were to even begin (since i want the button to be a image)
The code of my main page is this (its almost 1:1 with the first link), the data that i want to turn into a button is "LocationLink" which i temporary have setup as a Label
MainPage.xaml
<StackLayout>
<CollectionView ItemsSource="{Binding List}">
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout>
<BoxView HeightRequest="1" Color="#000000" IsVisible="true"/>
<Label Text="{Binding LocationName}"/>
<!-- Bellow is what i need help with-->
<Label Text="{Binding LocationLink}"/>
<Button/>
<!-- Above is what i need help with-->
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</StackLayout>
Item.cs
public class Temp
{
public Int Id { get; set; }
public string LocationName { get; set; }
public string LocationLink { get; set; }
}
<Button Text="Click Me!" Clicked="ButtonClick" />
then
protected void ButtonClick(object sender, EventArgs args)
{
// get the button
var button = (Button)sender;
// I don't know what your class is called, you will need to put
// the correct name here
var item = (MyListItem)button.BindingContext;
// now you can use item.LocationName, item.LocationLink, etc
// when calling the Share function
}

Xamarin form Command Parameter object is null passing from a template view

I am implementing list view with MVVM and have tap recogniser for the label. I have a custom template for displaying the cell. I am following tutorials on binding the command. I managed to bind the command but could not figure out how to bind Command Property. My command property is always null.
My list view is like this
***** For anyone looking for the solution. Before this line I had a grid view
like below
<Grid x:DataType="viewModels:CartViewModel">
************************************************
<ListView
ItemsSource="{Binding CartItem.Products}"
HasUnevenRows="True"
SeparatorVisibility="None"
VerticalOptions="FillAndExpand"
CachingStrategy="RecycleElement">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<templates:CartItemTemplate
RemoveItemCommand="{Binding BindingContext.RemoveCartItemCommand, Source={x:Reference Cart}}"
UpdateCartCommandParameter="{Binding .}"
AddCommentCommand="{Binding BindingContext.AddCommentCommand, Source={x:Reference Cart}}"
UpdateCartCommand="{Binding BindingContext.UpdateCartCommand, Source={x:Reference Cart}}"
/>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
And my template is like this. Other codes are omitted.
<Label
Text="Update cart"
TextDecorations="Underline"
Margin="8, 0, 0, 0"
FontSize="12"
VerticalOptions="Center">
<Label.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding UpdateCartCommand}"
CommandParameter="{Binding UpdateCartCommandParameter}"/>
</Label.GestureRecognizers>
</Label>
In the code behind of that template I have done like this
public static readonly BindableProperty UpdateCartCommandProperty =
BindableProperty.Create(nameof(UpdateCartCommand), typeof(ICommand), typeof(CartItemTemplate));
public ICommand UpdateCartCommand
{
get => (ICommand) GetValue(UpdateCartCommandProperty);
set => SetValue(UpdateCartCommandProperty, value);
}
public static BindableProperty UpdateCartCommandParameterProperty =
BindableProperty.Create(nameof(UpdateCartCommandParameter), typeof(Product), typeof(CartItemTemplate));
public Product UpdateCartCommandParameter
{
get => (Product) GetValue(UpdateCartCommandParameterProperty);
set => SetValue(UpdateCartCommandParameterProperty, value);
}
And finally in my MVVM code. I have implemented this.
public ICommand UpdateCartCommand { get; }
private readonly ICartService cartService;
private readonly INavigationService navigationService;
public CartPageViewModel(ICartService cartService, INavigationService navigationService)
{
this.cartService = cartService;
this.navigationService = navigationService;
UpdateCartCommand = new Command<object>(UpdateCartClicked);
}
private async void UpdateCartClicked(object cartItem)
{
await navigationService.ShowAlertAsync("Update Action", "Update cart quantity", "Ok");
}
The problem is object cartItem is always null. What am I doing wrong here? Any idea will be helpful. Thanks
You invoked the line like following which is illegal .
x:DataType="viewModels:CartPageViewModel"
Compiled bindings are currently disabled for any binding expressions that define the Source property. This is because the Source property is always set using the x:Reference markup extension, which can't be resolved at compile time.
If you want to set BindingContext in Xaml , use the following code
<ContentPage.BindingContext>
<viewModels:xxxViewModel/>
</ContentPage.BindingContext>

Xamarin.Forms ImageCell binding to a local file

I'm using data binding in a ListView to bind a list of ImageCells. The image is a file stored locally on the device as app data.
On Windows, using an absolute or relative path to the file does not work, I have to convert it to a file:// URI. Unfortunately, on Android, the file:// URI doesn't work, and it needs to be a path.
I'm currently working around the issue by using a different value in the view model depending on the target platform. Is there a better solution than this:
if (Device.OS == TargetPlatform.Windows) {
result.uri = new Uri(uri).AbsoluteUri;
}
Xaml:
<ListView.ItemTemplate>
<DataTemplate>
<ImageCell ImageSource="{Binding Uri}"
Text="{Binding Name}">
</ImageCell>
</DataTemplate>
</ListView.ItemTemplate>
The type of Uri is string, do I need to use a UriImageSource instead?
I solved it by creating converter and dependency service.
Xaml
<ContentPage.Content>
<StackLayout VerticalOptions="FillAndExpand" Padding="5,20,5,0" >
<ListView x:Name="list" ItemsSource="{Binding MyList}">
<ListView.ItemTemplate>
<DataTemplate>
<ImageCell Text="{Binding Name}" ImageSource="{Binding ImagePath, Converter={StaticResource AndroidImageInvert}}">
</ImageCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
Converter
public class ByteImageConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
string fileName = value as string;
return ImageSource.FromStream(() => new MemoryStream(DependencyService.Get<IWRDependencyService>().GetImageBytes(fileName)));
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Dependency Service
public byte[] GetImageBytes(string fileName)
{
fileName = fileName.Replace(".jpg", "").Replace(".png", "");
var resId = Forms.Context.Resources.GetIdentifier(
fileName.ToLower(), "drawable", Forms.Context.PackageName);
var icon = BitmapFactory.DecodeResource(Forms.Context.Resources, resId);
var ms = new MemoryStream();
icon.Compress(Bitmap.CompressFormat.Png, 0, ms);
byte[] bitmapData = ms.ToArray();
return bitmapData;
}

caliburn micro master detail

I have looked at all the Caliburn Micro stuff I can find and I think I'm simply confusing myself. I put together a simple sample as a test.
Model = Person.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WpfTestApp
{
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
}
PersonView.xaml
<UserControl x:Class="WpfTestApp.PersonView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding FirstName}" />
<TextBlock Text="{Binding LastName}" />
</StackPanel>
</Grid>
</UserControl>
ShellViewModel.cs
using Caliburn.Micro;
using System.ComponentModel.Composition;
namespace WpfTestApp {
[Export(typeof(IShell))]
public class ShellViewModel : PropertyChangedBase, IShell
{
public BindableCollection<PersonViewModel> Items { get; set; }
public ShellViewModel()
{
Items = new BindableCollection<PersonViewModel> {
new PersonViewModel(new Person { FirstName="Bart", LastName="Simpson" }),
new PersonViewModel(new Person { FirstName="Lisa", LastName="Simpson" }),
new PersonViewModel(new Person { FirstName="Homer", LastName="Simpson" }),
new PersonViewModel(new Person { FirstName="Marge", LastName="Simpson" }),
new PersonViewModel(new Person { FirstName="Maggie", LastName="Simpson" })
};
}
}
}
ShellView.xaml
<Window x:Class="WpfTestApp.ShellView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cal="http://www.caliburnproject.org">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel>
<ListBox x:Name="Items"/>
</StackPanel>
<ContentControl cal:View.Model="{Binding SelectedItem, Mode=TwoWay}" Grid.Row="1" />
</Grid>
</Window>
I am using the MEFBootstrapper as per the Caliburn Micro documentation.
1) Why is that when I select an item in the ListBox, nothing appears in the ContentControl. I am obviously missing something but I thought SelectedItem was hooked up by the conventions. I've tried using x:Name="ActiveItem" and that did not work either?
2) How does this work if my ShellViewModel.cs contained a BindableCollection of Person instead of PersonViewModel?
3) Can I name the BindableCollection something other than Items (Yes - I know Items is a convention of Caliburn Micro)?
Regards
Alan
Make your ContentControl in ShellView as
<ContentControl Name="ActiveItem"/>
And them inherit your ShellViewModel from Conductor.Collection.OneActive
public class ShellViewModel : Conductor<IScreen>.Collection.OneActive, IShell
{
}
Since Conductor already have Items Property for Binding ChildView, please remove your Items property from ShellViewModel.
Also we have to write code to activate the View in ShellView
Change ListBox to
<ListBox x:Name="Items" cal:Message.Attach="[Event SelectionChanged]=[Action ActivateChildView($this.SelectedItem)]"/>
and inside ShellViewModel a new method
public void ActiveChildView(object view)
{
ActiveItem(view);
}
I have not tested it, but Hope this will work.
You actually need a public property on your view model called SelectedItem (of type PersonViewModel in this case), otherwise there will be nothing to bind to, and you won't be able to access the bound data item from your view model.
You could bind to a collection of Person directly instead. You could say that this breaks the Law of Demeter, but if your PersonViewModel doesn't augment the Person model with any additional data, then you might consider a view model surplus in this case.
Yes you can name it anything, and the conventions will still work, e.g. Users will by convention map to SelectedUser (or ActiveUser or CurrentUser). People won't by convention map to SelectedPerson, but you can alter the ConventionManager Singularize delegate if you wanted this functionality.

Resources