问题描述
我正在尝试找到一种使用“表单行为”的好方法,以确保用户只能在输入控件中键入所需的输入。我的问题是xaml框架从未调用过OnDetachingFrom方法。这导致内存丢失,因为我订阅了Entry控件的TextChanged事件以修改其行为,并且无法取消订阅。
我试图找到一种“干净”的方法来跟踪当页面从堆栈中弹出时哪些控件需要清除其行为(我必须使用主NavigationPage来跟踪自己),但是我能想到的一切正在命名每个控件,将控件添加到后面代码中的Page上的集合中,并使用“ Clear”方法在Page上实现接口,该方法对集合中每个控件的xxx.Behaviors.Clear()进行调用OnDetachingForm每个控件的方法。
这似乎很可怕,与“干净”相反。我希望有人知道更好的方法。由于这样的设计疏忽,我从未真正喜欢XAML和MVVM。希望我所有的谷歌搜索都错过了一些东西。
就我的行为而言,我几乎是从Microsoft教程页面复制而来的。
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;
}
}
}
然后我只是做了一个基本的实现。
<?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>
那我该如何触发该事件?我唯一能找到的方法是致电
Field2.Behaviors.Clear();
但是我在哪里称呼它呢?我不能将其放在OnApprearing中,因为可以在导航到新页面时调用它,然后在再次显示此页面时,行为消失了。
/// <summary>
/// can be called when a new page is added to the stack
/// </summary>
protected override void Ondisappearing()
{
base.Ondisappearing();
}
因此,当从堆栈中删除页面时,我需要清除它。我怎么知道什么时候发生?我能找到的唯一方法是在MainPage的主导航页面上监听事件。
我还创建了一个接口IPagedispose并将其实现在我的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();
}
现在,我怎么知道要清除哪些控件?我做了这个复杂的过程,仍然必须命名控件并在后面的代码中跟踪它们?与仅在后面的代码中使用事件侦听器相比,这有什么好处?
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;
}
}
}
解决方法
感谢更新问题。
关于 OnDetachingFrom 方法,我们可以看一下此官方文档。
从控件中删除行为后,将触发
OnDetachingFrom
方法,该方法用于执行任何必需的清除操作,例如取消订阅事件以防止内存泄漏。 但是,除非通过Remove
或Clear
方法修改了控件的“行为”集合,否则不会从控件中隐式删除行为。
我们将看到,除非调用OnDetachingFrom
和Remove
方法,否则一般不会触发Clear
。
但是我在哪里称呼它呢?我不能将其放在OnApprearing中,因为可以在导航到新页面时调用它,然后在再次显示此页面时,行为消失了。
我们可以在页面的clear
方法上调用OnDisappearing
方法,但是在进入页面时也需要添加行为。
例如:
protected override void OnAppearing()
{
base.OnAppearing();
myentry.Behaviors.Add(new NumericValidationBehavior());
}
protected override void OnDisappearing()
{
base.OnDisappearing();
myentry.Behaviors.Clear();
}
==============================更新=============== ======================
您可以为Entry
使用 Style 和 Trigger ,这样就不必为每个{ {1}}通过编码。
创建一个Entry
类:
NumericValidationBehavior
然后在 ContentPage.Xaml 中:
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;
}
}
,
我将江小将发布的答案标记为答案。他的代码完全回答了我的问题。但是,在我的项目中,我决定牺牲“在背后的代码中没有代码”的理想,并选择更少的零件和更少的代码。我决定放弃行为,只创建一个基本ContentPage类,并为每种数据类型提供事件处理程序。然后,我可以在每个条目中聆听它们。尽管对XAML的“代码背后没有代码”并不忠实,但我更容易理解。如果我对这种方法有任何疑问,我很高兴有江青年的替代方案。谢谢!
using Xamarin.Forms;
namespace StoreTrak.Pages
{
public class _BaseContentPage : ContentPage
{
#region Entry Event Validation
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 _))
((Entry)sender).Text = e.OldTextValue;
}
protected void NullIntegerValidation_TextChanged(object sender,TextChangedEventArgs e)
{
if (string.IsNullOrEmpty(e.NewTextValue))
{
((Entry)sender).Text = "";
return;
}
if (!int.TryParse(e.NewTextValue,out int _))
((Entry)sender).Text = e.OldTextValue;
}
#endregion Entry Event Validation
}
}
<?xml version="1.0" encoding="utf-8" ?>
<d:_BaseContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="clr-namespace:StoreTrak.Pages;assembly=StoreTrak"
xmlns:vm="clr-namespace:StoreTrak.ViewModels"
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>
<!-- Field1 - string -->
<Label Text="Field 1" Grid.Row="0" Grid.Column="0" />
<StackLayout Grid.Row="0" Grid.Column="1" Margin="0" Padding="0">
<Entry Text="{Binding Field1}" Grid.Row="0" Grid.Column="1" />
<Label Text="Error number 1" TextColor="Red" FontSize="Small" IsVisible="False" />
</StackLayout>
<!-- Field2 -int -->
<Label Text="Field 2" Grid.Row="2" Grid.Column="0" />
<StackLayout Grid.Row="2" Grid.Column="1" Margin="0" Padding="0">
<Entry Text="{Binding Field2}" TextChanged="IntegerValidation_TextChanged" />
<Label Text="Error number 2" TextColor="Red" FontSize="Small" IsVisible="False" />
</StackLayout>
<!-- Field3 -int? -->
<Label Text="Field 3" Grid.Row="3" Grid.Column="0" />
<StackLayout Grid.Row="3" Grid.Column="1" Margin="0" Padding="0">
<Entry Text="{Binding Field3}" TextChanged="NullIntegerValidation_TextChanged" />
<Label Text="Error number 3" TextColor="Red" FontSize="Small" IsVisible="False" />
</StackLayout>
</Grid>
</d:_BaseContentPage>
using Xamarin.Forms.Xaml;
namespace StoreTrak.Pages
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class TestPage : _BaseContentPage
{
public TestPage()
{
InitializeComponent();
}
}
}