I'm trying to achieve having a button just like this screenshot:
So i did a custom control with specific renderer for iOS and Android, the control is below:
public class ElevationButton : Button
{
public float Elevation
{
get => (float)GetValue(ElevationProperty);
set => SetValue(ElevationProperty, value);
}
public static BindableProperty ElevationProperty = BindableProperty.Create(nameof(Elevation), typeof(float), typeof(ElevationButton), 4.0f);
}
and here is the iOS renderer:
public class ElevationButtonRenderer : ButtonRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
{
base.OnElementChanged(e);
if (e.NewElement == null)
return;
}
public override void Draw(CGRect rect)
{
base.Draw(rect);
UpdateShadow();
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
var element = (ElevationButton)this.Element;
if (e.PropertyName == nameof(element.Elevation))
UpdateShadow();
}
private void UpdateShadow()
{
var element = (ElevationButton)this.Element;
Layer.ShadowRadius = element.Elevation;
Layer.ShadowColor = UIColor.Gray.CGColor;
Layer.ShadowOffset = new CGSize(2, 2);
Layer.ShadowOpacity = 0.80f;
Layer.ShadowPath = UIBezierPath.FromRect(Layer.Bounds).CGPath;
Layer.MasksToBounds = false;
}
}
and the Android one:
public class ElevationButtonRenderer : Xamarin.Forms.Platform.Android.AppCompat.ButtonRenderer
{
public ElevationButtonRenderer(Context context) : base(context) { }
protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
{
base.OnElementChanged(e);
if (e.NewElement == null)
return;
var element = (ElevationButton)this.Element;
// we need to reset the StateListAnimator to override the setting of Elevation on touch down and release.
Control.StateListAnimator = new Android.Animation.StateListAnimator();
// set the elevation manually
ViewCompat.SetElevation(this, element.Elevation);
ViewCompat.SetElevation(Control, element.Elevation);
}
public override void Draw(Canvas canvas)
{
var element = (ElevationButton)this.Element;
Control.Elevation = element.Elevation;
base.Draw(canvas);
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
var element = (ElevationButton)this.Element;
if (e.PropertyName == nameof(element.Elevation))
{
ViewCompat.SetElevation(this, element.Elevation);
ViewCompat.SetElevation(Control, element.Elevation);
UpdateLayout();
}
}
}
Here is the xaml where i declare the custom control:
<StackLayout Orientation="Horizontal">
<StackLayout Orientation="Vertical">
<controls:ElevationButton x:Name="buttonTest"
BackgroundColor="White"
HeightRequest="40"
WidthRequest="40"
Elevation="20"
CornerRadius="50"
Padding="10,10,10,10"
Text="{x:Static fa:FontAwesomeIcons.Camera}"
FontFamily="FASolid"
TextColor="{StaticResource Text}" />
<Label Text="PHOTO" />
</StackLayout>
So when I'm testing this code, the render is like this not displaying any Elevation and shadow, no matter which value i'm putting for Elevation property :
I saw on other posts there are some issues with CornerRadius, in my case with or without it the Elevation isn't displaying.
If you have any idea i would be glad to read it.
If you just want shadow, you could use Community Toolkit.
Install from NuGet: https://www.nuget.org/packages/Xamarin.CommunityToolkit/2.0.1?_src=template
Xaml:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:App1"
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
x:Class="App1.Page6">
<StackLayout Orientation="Horizontal">
<StackLayout Orientation="Vertical">
<controls:ElevationButton x:Name="buttonTest"
BackgroundColor="White"
.......
xct:ShadowEffect.Color="Red"
xct:ShadowEffect.OffsetY="12" />
<Label Text="PHOTO" />
</StackLayout>
</StackLayout>
</ContentPage>
Related
How to use multiple TabBars (Bottom) using Shell in single application so that different page have different TabBar. Could i use multiple Shell or is it possible with single shell.
You can use one Shell application , in this example Home has 3 tabs. The Settings icon a TabbedPage with 3 tabs.
Shell.xaml
<Shell.FlyoutFooterTemplate>
<DataTemplate>
<Grid RowDefinitions="30" ColumnDefinitions="150, 150">
<Image
Grid.Row="0"
Grid.Column="0"
Source="Settings.png"
HorizontalOptions="StartAndExpand"
Margin="50,0,0,0"
>
<Image.GestureRecognizers>
<TapGestureRecognizer
Tapped="TapGestureRecognizer_Tapped"
NumberOfTapsRequired="1" />
</Image.GestureRecognizers>
</Image>
<Image
Grid.Row="0"
Grid.Column="1"
Source="Power.png"
HorizontalOptions="EndAndExpand"
Margin="0,0,30,0">
<Image.GestureRecognizers>
<TapGestureRecognizer
Tapped="TapGestureRecognizer_Tapped_1"
NumberOfTapsRequired="1" />
</Image.GestureRecognizers>
</Image>
</Grid>
</DataTemplate>
</Shell.FlyoutFooterTemplate>
Shell.xaml.cs
public AppShell()
{
InitializeComponent();
Routing.RegisterRoute(nameof(ItemDetailPage), typeof(ItemDetailPage));
Routing.RegisterRoute(nameof(NewItemPage), typeof(NewItemPage));
Routing.RegisterRoute(nameof(StartPage), typeof(StartPage));
Routing.RegisterRoute(nameof(HomePage), typeof(HomePage));
}
private async void OnMenuItemClicked(object sender, EventArgs e)
{
await Shell.Current.GoToAsync("ItemDetailPage");
Shell.Current.FlyoutIsPresented = false;
}
private async void MenuItem_Clicked(object sender, EventArgs e)
{
await Shell.Current.GoToAsync("//LoginPage");
}
private async void TapGestureRecognizer_Tapped(object sender, EventArgs e)
{
Routing.RegisterRoute(nameof(SettingsPage), typeof(SettingsPage));
await Shell.Current.GoToAsync("SettingsPage");
Shell.Current.FlyoutIsPresented = false;
}
private void TapGestureRecognizer_Tapped_1(object sender, EventArgs e)
{
/// do stuff what you want
}
}
A easy way is to conbine the FlyoutPage and TabbedPage.
You could create a FlyoutPage. It would generate FlyoutPage1, FlyoutPage1Detail, FlyoutPage1Flyout and FlyoutPage1FlyoutMenuItem. You could create two or more TabbedPages and make the changes like below.
FlyoutPage1FlyoutMenuItem:
public class FlyoutPage1FlyoutMenuItem
{
public int Id { get; set; }
public string Title { get; set; }
public string PageName { get; set; }
}
FlyoutPage1Flyout.xaml.cs:
private class FlyoutPage1FlyoutViewModel : INotifyPropertyChanged
{
public ObservableCollection<FlyoutPage1FlyoutMenuItem> MenuItems { get; set; }
public FlyoutPage1FlyoutViewModel()
{
MenuItems = new ObservableCollection<FlyoutPage1FlyoutMenuItem>(new[]
{
new FlyoutPage1FlyoutMenuItem { Id = 0, Title = "MainPage", PageName="MainPage"},
new FlyoutPage1FlyoutMenuItem { Id = 1, Title = "TabbedPage1", PageName="TabbedPage1" },
new FlyoutPage1FlyoutMenuItem { Id = 2, Title = "TabbedPage2", PageName="TabbedPage2" },
});
}
#region INotifyPropertyChanged Implementation
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
if (PropertyChanged == null)
return;
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
FlyoutPage1.xaml.cs:
private void ListView_ItemSelected(object sender, SelectedItemChangedEventArgs e)
{
var item = e.SelectedItem as FlyoutPage1FlyoutMenuItem;
if (item == null)
return;
var pageType = Type.GetType($"App1.{item.PageName}");
var page = (Page)Activator.CreateInstance(pageType);
page.Title = item.Title;
Detail = new NavigationPage(page);
IsPresented = false;
FlyoutPage.ListView.SelectedItem = null;
}
I have a collectionview that is bound to an ObservableRangeCollectionin my ViewModel.
In my ViewModel there is a Method that runs onAppearing and I want my ColletionViewto be filled from there, but when I do so the collectionveiw dose not display the content only when i reload the content is shown.
View:
<RefreshView Grid.Row="1"
Grid.RowSpan="2"
Command="{Binding RefreshCommand}"
IsRefreshing="{Binding IsBusy, Mode=OneWay}">
<RefreshView.RefreshColor>
<OnPlatform x:TypeArguments="Color">
<On Platform="iOS" Value="White"/>
</OnPlatform>
</RefreshView.RefreshColor>
<CollectionView x:Name="Collection"
ItemsSource="{Binding Locations, Mode=OneWay}"
ItemTemplate="{StaticResource ListDataTemplate}"
RemainingItemsThresholdReachedCommand="{Binding LoadMoreCommand}"
RemainingItemsThreshold="10"
SelectionMode="Single"
BackgroundColor="Transparent"
ItemsLayout="VerticalList"
SelectedItem="{Binding SelectedItem}"
SelectionChangedCommand="{Binding SelectedCommand}">
<CollectionView.EmptyView>
<StackLayout Padding="12">
<Label HorizontalOptions="Center" Text="Keine Daten vorhanden!" TextColor="White"/>
</StackLayout>
</CollectionView.EmptyView>
</CollectionView>
</RefreshView>
ViewModel:
namespace YourPartys.ViewModels
{
public class ListViewModel : ViewModelBase
{
#region Variables
#endregion
#region Propertys
LocationModel selectedItem;
public LocationModel SelectedItem
{
get => selectedItem;
set => SetProperty(ref selectedItem, value);
}
public ObservableRangeCollection<LocationModel> Locations { get;set; } = new ObservableRangeCollection<LocationModel>();
double distance;
public double Distance
{
get => distance;
set => SetProperty(ref distance, value);
}
#endregion
#region Commands
public ICommand FilterButtonCommand { get; }
public ICommand RefreshCommand { get; }
public ICommand SelectedCommand { get; }
public ICommand LoadMoreCommand { get; }
#endregion
//Constructor
public ListViewModel()
{
FilterButtonCommand = new Command(OpenFilter);
RefreshCommand = new AsyncCommand(Refresh);
SelectedCommand = new AsyncCommand(Select);
}
public override async void VModelActive(Page sender, EventArgs eventArgs)
{
base.VModelActive(sender, eventArgs);
var locs = await FirestoreService.GetLocations("Locations");
Locations.AddRange(locs);
}
private void OpenFilter(object obj)
{
PopupNavigation.Instance.PushAsync(new ListFilterPage());
}
private async Task Refresh()
{
IsBusy = true;
var locs = await FirestoreService.GetLocations("Locations");
Locations.AddRange(locs);
IsBusy = false;
}
private async Task Select()
{
if (SelectedItem == null)
return;
var route = $"{nameof(DetailPage)}?Locationid={SelectedItem.Locationid}";
SelectedItem = null;
await AppShell.Current.GoToAsync(route);
}
}
}
There are several problems in your demo.
1.Since you set the BindingContext for your page in xaml as follows:
<ContentPage.BindingContext>
<viewmodels:MainViewModel/>
</ContentPage.BindingContext>
you didn't need to recreate another object MainViewModel in a CS file and reference it. These are two different objects.
MainViewModel viewModel;
viewModel = new MainViewModel();
protected override void OnAppearing()
{
base.OnAppearing();
viewModel.VModelActive(this, EventArgs.Empty);
}
So, you can get the BindingContext in MainPage.xaml.cs in function OnAppearing as follows:
protected override void OnAppearing()
{
base.OnAppearing();
viewModel = (MainViewModel)this.BindingContext;
viewModel.VModelActive(this, EventArgs.Empty);
}
The whole code is
public partial class MainPage : ContentPage
{
MainViewModel viewModel;
public MainPage()
{
InitializeComponent();
// viewModel = new MainViewModel();
}
protected override void OnAppearing()
{
base.OnAppearing();
viewModel = (MainViewModel)this.BindingContext;
viewModel.VModelActive(this, EventArgs.Empty);
}
}
2.when we set the text color of the Label to White,this makes it hard to see the text,so you can reset it to another color,for example Black:
<Label Text="{Binding Name}"
FontSize="30"
TextColor="White"/>
Please check the GIF for the problem.
I am actually using two imagebutton here and change IsVisible, since I couldn't accomplish swapping the image by Binding on the source.
ViewModel:
public bool IsAudioPlaying
{
get => player.IsPlaying;
}
...
public void PlayOrPause()
{
if (player.IsPlaying)
player.Pause();
else
player.Play();
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("IsAudioPlaying"));
}
XAML:
<ImageButton AbsoluteLayout.LayoutBounds=".5, 0, 100, 100" AbsoluteLayout.LayoutFlags="PositionProportional" Source="play.png" Padding="20" WidthRequest="80" HeightRequest="80"
CornerRadius="40" VerticalOptions="Center" HorizontalOptions ="Center" BackgroundColor="#cea448" Clicked="PlayOrPause" Margin="10" IsVisible="{Binding IsAudioPlaying, Converter={StaticResource InverseBoolConverter}}" />
<ImageButton AbsoluteLayout.LayoutBounds=".5, 0, 100, 100" AbsoluteLayout.LayoutFlags="PositionProportional" Source="pause.png" Padding="20" WidthRequest="80" HeightRequest="80"
CornerRadius="40" VerticalOptions="Center" HorizontalOptions ="Center" BackgroundColor="#cea448" Clicked="PlayOrPause" Margin="10" IsVisible="{Binding IsAudioPlaying}" />
I am actually using two imagebutton here and change IsVisible, since I couldn't accomplish swapping the image by Binding on the source.
Create a View Model.
public class ViewModel : INotifyPropertyChanged
{
private bool _isAudioPlaying;
public bool IsAudioPlaying
{
get
{
return _isAudioPlaying;
}
set
{
_isAudioPlaying = value;
OnPropertyChanged("IsAudioPlaying");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Use ObservableCollection to update.
ObservableCollection<ViewModel> observableCollection { get; set; }
public MainPage()
{
InitializeComponent();
observableCollection = new ObservableCollection<ViewModel>()
{
new ViewModel(){ IsAudioPlaying=true}
};
this.BindingContext = observableCollection;
}
private void PlayOrPause(object sender, EventArgs e)
{
if (observableCollection[0].IsAudioPlaying == true)
{
observableCollection[0].IsAudioPlaying = false;
imageButton.Source = "pause.png";
}
else
{
observableCollection[0].IsAudioPlaying = true;
imageButton.Source = "play.png";
}
}
I want to bind a CustomLabel to a VM by creating a new bindable property.
In OneWay mode, the first VM data has properly changed the property of the CustomLabel. but It didn't work from second time.
Although The VM event has occur, the Bindable Property of CustomView has not fired its PropertyChanged event.
It works properly in TwoWay mode though.
I've been testing for two days and searching for the cause, but I coudn't find it well.
Anybody tell me how to do?
// HomeViewModel.cs
public class HomeViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _customName = "-";
public string CustomName
{
get
{
Debug.WriteLine("Get_CustomName");
return _customName;
}
set
{
if (value != _customName)
{
Debug.WriteLine("Set_CustomName");
_customName = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CustomName)));
}
}
}
}
// MainPage.cs
public partial class MainPage : ContentPage
{
HomeViewModel Vm = new HomeViewModel();
public MainPage()
{
InitializeComponent();
BindingContext = Vm;
}
void ButtonTrue_Clicked(object sender, EventArgs e)
{
Vm.CustomName = "True";
}
void ButtonFalse_Clicked(object sender, EventArgs e)
{
Vm.CustomName = "False";
}
}
<!-- MainPage.xaml -->
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Ex_Binding"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="Ex_Binding.MainPage">
<StackLayout Padding="50,0" VerticalOptions="Center">
<StackLayout Orientation="Horizontal" HorizontalOptions="Center">
<Label Text="Custom Result : " />
<local:CustomLabel x:Name="lbCustom" MyText="{Binding CustomName}" HorizontalOptions="Center" />
</StackLayout>
<StackLayout Orientation="Horizontal">
<Button Text="TRUE" BackgroundColor="LightBlue" HorizontalOptions="FillAndExpand" Clicked="ButtonTrue_Clicked" />
<Button Text="FALSE" BackgroundColor="LightPink" HorizontalOptions="FillAndExpand" Clicked="ButtonFalse_Clicked" />
</StackLayout>
</StackLayout>
</ContentPage>
// CustomLabel.cs
public class CustomLabel : Label
{
public static readonly BindableProperty MyTextProperty = BindableProperty.Create(nameof(MyText), typeof(string), typeof(CustomLabel), null, BindingMode.OneWay, propertyChanged: OnMyTextChanged);
private static void OnMyTextChanged(BindableObject bindable, object oldValue, object newValue)
{
var thisBindable = (CustomLabel)bindable;
if (thisBindable != null)
{
thisBindable.MyText = (string)newValue;
}
}
public string MyText
{
get => (string)GetValue(MyTextProperty);
set
{
SetValue(MyTextProperty, value);
Text = value;
}
}
}
Cause :
thisBindable.MyText = (string)newValue;
Because you set the value of MyText when its value changed . So it will never been invoked next time (in TwoWay the method will been invoked multi times).
Solution:
You should set the Text in OnMyTextChanged directly .
private static void OnMyTextChanged(BindableObject bindable, object oldValue, object newValue)
{
var thisBindable = (CustomLabel)bindable;
if (thisBindable != null)
{
thisBindable.Text = (string)newValue;
}
}
public string MyText
{
get => (string)GetValue(MyTextProperty);
set
{
SetValue(MyTextProperty, value);
//Text = value;
}
}
I have a ListView with multiple entries. In each, i should enter a value which would be compared to another value (the value is valid if higher or equal).
I must implement a solution that can check if all entries in ListView are valid when i tap a submit button.
I implemented a kind of validation using behaviors but it just highlights the entry value when it's not valid and it doesn't affect the button.
<ListView x:Name="lstRegistadores" ItemsSource="{Binding Contador.Registadores}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Entry x:Name="txtValue" Text="{Binding Corrente.Valor}" >
<Entry.Behaviors>
<local:RegistadorValidatorBehavior />
</Entry.Behaviors>
</Entry>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Button x:Name="btnSubmit" Text="Confirmar" />
public class RegistadorValidatorBehavior : Behavior<Entry>
{
static readonly BindablePropertyKey IsValidPropertyKey = BindableProperty.CreateReadOnly("IsValid", typeof(bool), typeof(RegistadorValidatorBehavior), false);
public static readonly BindableProperty IsValidProperty = IsValidPropertyKey.BindableProperty;
public bool IsValid
{
get { return (bool)base.GetValue(IsValidProperty); }
private set { base.SetValue(IsValidPropertyKey, value); }
}
protected override void OnAttachedTo(Entry bindable)
{
bindable.TextChanged += HandleTextChanged;
base.OnAttachedTo(bindable);
}
protected override void OnDetachingFrom(Entry bindable)
{
bindable.TextChanged -= HandleTextChanged;
base.OnDetachingFrom(bindable);
}
void HandleTextChanged(object sender, TextChangedEventArgs e)
{
var entry = sender as Entry;
var registador = (Registador)entry.BindingContext;
var compareTo = registador.Ultimo.Valor;
int valor;
IsValid = (int.TryParse(e.NewTextValue,out valor) && valor >= compareTo) ;
entry.TextColor = IsValid ? Color.Default : Color.Red;
}
}
Can anyone show me the way to go?
I would recomend you to check FluentValidation and apply it to Contador.Registadores.
FluentValidation is one of those essentials libraries that works very well with Xamarin.
This article should give you insights how FluetValidation can help you to check you entire list and complex subproperties:
https://github.com/JeremySkinner/FluentValidation/wiki/b.-Creating-a-Validator