Issue
I am trying to find a good way to use Form Behaviors to make sure a user can only type desired input into an entry control. My issue is that the OnDetachingFrom method is never called by the xaml framework. This results in memory loss because I subscribe to the TextChanged event of the Entry control to modify its behavior and I cannot unsubscribe.
I have tried to find a 'clean' way of keeping track of which controls need to have their behaviors cleared when the Page is Popped off the stack (which I have to keep track of myself using the main NavigationPage) but all I can think of is naming each control, adding the controls to a collection on the Page in code behind, implementing an interface on the Page with a 'Clear' method which does a xxx.Behaviors.Clear() on each control in the collection which calls the OnDetachingForm method for each control.
This seems kind of horrible and the opposite of 'clean'. I was hoping someone knew a better way. I never really liked XAML and MVVM because of design oversights like this. Hopefully all my Googling just missed something.
For my behavior I just pretty much copied from the Microsoft tutorial page.
https://docs.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/behaviors/creating
namespace StoreTrak.Behaviors
{
public class IntegerValidationBehavior : Behavior<Entry>
{
protected override void OnAttachedTo(Entry bindable)
{
if (bindable != null)
bindable.TextChanged += OnEntryTextChanged;
base.OnAttachedTo(bindable);
}
/// <summary>
/// This NEVER gets called by the XAML framework.
/// </summary>
/// <param name="bindable"></param>
protected override void OnDetachingFrom(Entry bindable)
{
if (bindable != null)
bindable.TextChanged -= OnEntryTextChanged;
base.OnDetachingFrom(bindable);
}
private static void OnEntryTextChanged(object sender, TextChangedEventArgs args)
{
if (string.IsNullOrEmpty(args.NewTextValue))
{
((Entry)sender).Text = "0";
return;
}
if (!int.TryParse(args.NewTextValue, out int x))
((Entry)sender).Text = args.OldTextValue;
}
}
}
Then I just did a basic implementation.
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:StoreTrak.ViewModels"
xmlns:behaviors="clr-namespace:StoreTrak.Behaviors"
x:Class="StoreTrak.Pages.TestPage">
<ContentPage.BindingContext>
<vm:TestViewModel />
</ContentPage.BindingContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Label Text="Field 1" Grid.Row="0" Grid.Column="0" />
<Entry Text="{Binding Field1}" Grid.Row="0" Grid.Column="1" />
<Label Text="Field 2" Grid.Row="2" Grid.Column="0" />
<StackLayout Grid.Row="2" Grid.Column="1" Margin="0" Padding="0">
<Entry x:Name="Field2" Text="{Binding Field2}">
<Entry.Behaviors>
<behaviors:IntegerValidationBehavior />
</Entry.Behaviors>
</Entry>
<Label Text="Error number 1" TextColor="Red" FontSize="Small" IsVisible="False" />
<Label Text="Error number 2" TextColor="Red" FontSize="Small" IsVisible="True" />
<Label Text="Error number 3" TextColor="Red" FontSize="Small" IsVisible="False" />
</StackLayout>
<Label Text="Field 3" Grid.Row="3" Grid.Column="0" />
<Entry Text="{Binding Field3}" Grid.Row="3" Grid.Column="1" />
</Grid>
So where how do I get that event to fire? The only way I could find was to call
Field2.Behaviors.Clear();
But where do I call it? I can't put it in OnApprearing because it can be called when a new page is navigated to, then when this page is shown again the behaviors are gone.
/// <summary>
/// can be called when a new page is added to the stack
/// </summary>
protected override void OnDisappearing()
{
base.OnDisappearing();
}
So I need to clear it when the page is removed from the stack. How do I know when that happens? The only way I could find was to listen to an event on the main navigation page way back in the MainPage.
I also create an Interface IPageDispose and implement it on my Page.
public partial class App : Application
{
public App()
{
InitializeComponent();
MainPage = new NavigationPage(new Pages.MainPage());
if (MainPage is NavigationPage page)
{
page.Popped += Page_Popped;
page.PoppedToRoot += Page_PoppedToRoot;
}
}
/// <summary>
/// https://www.johankarlsson.net/2017/08/popped-pages-in-xamarin-forms.html
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Page_PoppedToRoot(object sender, NavigationEventArgs e)
{
if (e is PoppedToRootEventArgs args)
{
foreach(Page page in args.PoppedPages)
Page_Popped(sender, new NavigationEventArgs(page));
}
}
private void Page_Popped(object sender, NavigationEventArgs e)
{
if (e.Page is IPageDispose ipd)
{
ipd.Dispose();
}
}
protected override void OnStart()
{
}
protected override void OnSleep()
{
}
protected override void OnResume()
{
}
public async static void HandleError(Exception ex)
{
Logger.Entry(ex);
await Application.Current.MainPage.Navigation.PushModalAsync(new Pages.LogPages.LogPage(ex));
}
}
}
namespace StoreTrak.Pages
{
public interface IPageDispose
{
void Dispose();
}
}
public partial class TestPage : ContentPage, IPageDispose
{
public TestPage()
{
InitializeComponent();
}
public void Dispose()
{
// How do I know which control to clear?
// give each one a name and hardcode the Clear method?
throw new NotImplementedException();
}
Now, how do I know which controls to clear? I made this complicated process and still have to name the controls and keep track of them in the code behind? How is that better than just using an event listener in the code behind?
public class _BasePage : ContentPage
{
protected void IntegerValidation_TextChanged(object sender, TextChangedEventArgs e)
{
if (string.IsNullOrEmpty(e.NewTextValue))
{
((Entry)sender).Text = "0";
return;
}
if (!int.TryParse(e.NewTextValue, out int x))
((Entry)sender).Text = e.OldTextValue;
}
}
}
Solution
Thanks for updating the question.
About OnDetachingFrom method , we can have a look at this official document.
The
OnDetachingFrom
method is fired when a behavior is removed from a control, and is used to perform any required cleanup such as unsubscribing from an event to prevent a memory leak. However, behaviors are not implicitly removed from controls unless the control's Behaviors collection is modified by aRemove
orClear
method.
We will see that OnDetachingFrom
will not be fired generally unless call Remove
and Clear
method.
But where do I call it? I can't put it in OnApprearing because it can be called when a new page is navigated to, then when this page is shown again the behaviors are gone.
We can call clear
method on OnDisappearing
method of page, however also need to add behavior when entering the page.
For example:
protected override void OnAppearing()
{
base.OnAppearing();
myentry.Behaviors.Add(new NumericValidationBehavior());
}
protected override void OnDisappearing()
{
base.OnDisappearing();
myentry.Behaviors.Clear();
}
===============================Update======================================
You can use Style and Trigger for Entry
, then will not need to add/chear behavior for each Entry
by coding.
Create a NumericValidationBehavior
class :
public class NumericValidationBehavior : Behavior<Entry>
{
public static readonly BindableProperty AttachBehaviorProperty =
BindableProperty.CreateAttached ("AttachBehavior", typeof(bool), typeof(NumericValidationBehavior), false, propertyChanged: OnAttachBehaviorChanged);
public static bool GetAttachBehavior (BindableObject view)
{
return (bool)view.GetValue (AttachBehaviorProperty);
}
public static void SetAttachBehavior (BindableObject view, bool value)
{
view.SetValue (AttachBehaviorProperty, value);
}
static void OnAttachBehaviorChanged (BindableObject view, object oldValue, object newValue)
{
var entry = view as Entry;
if (entry == null) {
return;
}
bool attachBehavior = (bool)newValue;
if (attachBehavior) {
entry.Behaviors.Add (new NumericValidationBehavior ());
} else {
var toRemove = entry.Behaviors.FirstOrDefault (b => b is NumericValidationBehavior);
if (toRemove != null) {
entry.Behaviors.Remove (toRemove);
}
}
}
protected override void OnAttachedTo (Entry entry)
{
entry.TextChanged += OnEntryTextChanged;
base.OnAttachedTo (entry);
}
protected override void OnDetachingFrom (Entry entry)
{
entry.TextChanged -= OnEntryTextChanged;
base.OnDetachingFrom (entry);
}
void OnEntryTextChanged (object sender, TextChangedEventArgs args)
{
double result;
bool isValid = double.TryParse (args.NewTextValue, out result);
((Entry)sender).TextColor = isValid ? Color.Default : Color.Red;
}
}
Then in ContentPage.Xaml:
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:WorkingWithBehaviors;assembly=NumericValidationBehaviorStyle" x:Class="WorkingWithBehaviors.NumericValidationPage" Title="XAML" IconImageSource="xaml.png">
<ContentPage.Resources>
<ResourceDictionary>
<Style TargetType="Entry">
<Style.Triggers>
<Trigger TargetType="Entry"
Property="IsFocused"
Value="True">
<Setter Property="local:NumericValidationBehavior.AttachBehavior"
Value="true" />
<!-- multiple Setters elements are allowed -->
</Trigger>
<Trigger TargetType="Entry"
Property="IsFocused"
Value="False">
<Setter Property="local:NumericValidationBehavior.AttachBehavior"
Value="False" />
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
</ContentPage.Resources>
<StackLayout Padding="10,50,10,0">
<Label Text="Red when the number isn't valid" FontSize="Small" />
<Entry Placeholder="Enter a System.Double" />
</StackLayout>
</ContentPage>
Answered By - Junior Jiang
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.