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;
}
}
}