在 WindowsPhone7模拟器上两次触发 TextBox.TextChanged 事件

我有一个非常简单的测试应用程序,只是玩玩 Windows Phone 7。我刚刚在标准 UI 模板中添加了一个 TextBox和一个 TextBlock。唯一的自定义代码如下:

public partial class MainPage : PhoneApplicationPage
{
public MainPage()
{
InitializeComponent();
}


private int counter = 0;


private void TextBoxChanged(object sender, TextChangedEventArgs e)
{
textBlock1.Text += "Text changed " + (counter++) + "\r\n";
}
}

TextBox.TextChanged事件在 XAML 中连接到 TextBoxChanged:

<TextBox Height="72" HorizontalAlignment="Left" Margin="6,37,0,0"
Name="textBox1" Text="" VerticalAlignment="Top"
Width="460" TextChanged="TextBoxChanged" />

然而,每次我在模拟器中运行时按下一个键(屏幕上的键盘或物理键盘,按下暂停以启用后者) ,计数器就会增加两次,在 TextBlock中显示两行。我试过的所有方法都表明这个事件真的发生了两次,我不知道为什么。我已经验证了它只被订阅一次——如果我在 MainPage构造函数中取消订阅,当文本更改时什么都不会发生(对于文本块)。

我已经在一个常规的 Silverlight 应用程序中尝试了相同的代码,但是没有出现这种情况。我现在没有实体手机来重现这一切。我没有发现任何记录,这是一个已知的问题在 Windows Phone 7。

有人能解释一下我做错了什么吗,或者我应该把这个当作一个错误来报告?

编辑: 为了减少使用两个文本控件的可能性,我尝试完全删除 TextBlock,并将 TextBoxChanged 方法更改为 只是递增 counter。然后我在模拟器中运行,键入10个字母,那么counter++;行上放置一个断点(只是为了排除破坏调试器导致问题的任何可能性)——它将 counter显示为20。

编辑: 我现在已经 在 Windows Phone 7论坛上问道... 我们将看到会发生什么。

14839 次浏览

Sure looks like a bug to me, if you're trying to raise an event every time the text changes you could try using a two-way binding instead, unfortunately this won't raise per-key press change events (only when the field loses focus). Here's a workaround if you need one:

        this.textBox1.TextChanged -= this.TextBoxChanged;
textBlock1.Text += "Text changed " + (counter++) + "\r\n";
this.textBox1.TextChanged += this.TextBoxChanged;

i'd go for the bug, mainly because if you put the KeyDown and KeyUp events in there, it shows that that they are fired only once (each of them) but the TextBoxChanged event is fired twice

Disclaimer- I'm not familiar with xaml nuances and I know this sounds illogical... but anyway- my first thought is to try passing as just plain eventargs rather than textchangedeventargs. Doesn't make sense, but may be it could help? It seems like when I've seen double firings like this before that it is either due to a bug or due to somehow 2 add event handler calls happening behind the scenes... I'm not sure which though?

If you need quick and dirty, again, me not being experienced with xaml- my next step would be to just skip xaml for that textbox as a quick workaround... do that textbox totally in c# for now until you can pinpoint the bug or tricky code... that is, if you need a temporary solution.

I believe this has always been a bug in the Compact Framework. It must have been carried over into WP7.

That does sound like a bug to me. As a workaround, you could always use Rx's DistinctUntilChanged. There is an overload that allows you to specify the distinct key.

This extension method returns the observable TextChanged event but skips consecutive duplicates:

public static IObservable<IEvent<TextChangedEventArgs>> GetTextChanged(
this TextBox tb)
{
return Observable.FromEvent<TextChangedEventArgs>(
h => textBox1.TextChanged += h,
h => textBox1.TextChanged -= h
)
.DistinctUntilChanged(t => t.Text);
}

Once the bug is fixed you can simply remove the DistinctUntilChanged line.

I dont think it is a bug .. When you assign the value to a text property inside the textchanged event , the textbox value is changed which will again call the text changed event ..

try this in Windows Forms Application , you might get an error

"An unhandled exception of type 'System.StackOverflowException' occurred in System.Windows.Forms.dll"

The reason the TextChanged event fires twice in WP7 is a side effect of how the TextBox has been templated for the Metro look.

If you edit the TextBox template in Blend you will see that it contains a secondary TextBox for the disabled/read-only state. This causes, as a side effect, the event to fire twice.

You can change the template to remove the extra TextBox (and associated states) if you don't need these states, or modify the template to achieve a different look in the disabled/read-only state, without using a secondary TextBox.

With that, the event will fire only once.

StefanWick is right, consider using this template

<Application.Resources>
<ControlTemplate x:Key="PhoneDisabledTextBoxTemplate" TargetType="TextBox">
<ContentControl x:Name="ContentElement" BorderThickness="0" HorizontalContentAlignment="Stretch" Margin="{StaticResource PhoneTextBoxInnerMargin}" Padding="{TemplateBinding Padding}" VerticalContentAlignment="Stretch"/>
</ControlTemplate>
<Style x:Key="TextBoxStyle1" TargetType="TextBox">
<Setter Property="FontFamily" Value="{StaticResource PhoneFontFamilyNormal}"/>
<Setter Property="FontSize" Value="{StaticResource PhoneFontSizeMediumLarge}"/>
<Setter Property="Background" Value="{StaticResource PhoneTextBoxBrush}"/>
<Setter Property="Foreground" Value="{StaticResource PhoneTextBoxForegroundBrush}"/>
<Setter Property="BorderBrush" Value="{StaticResource PhoneTextBoxBrush}"/>
<Setter Property="SelectionBackground" Value="{StaticResource PhoneAccentBrush}"/>
<Setter Property="SelectionForeground" Value="{StaticResource PhoneTextBoxSelectionForegroundBrush}"/>
<Setter Property="BorderThickness" Value="{StaticResource PhoneBorderThickness}"/>
<Setter Property="Padding" Value="2"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Grid Background="Transparent">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates" ec:ExtendedVisualStateManager.UseFluidLayout="True">
<VisualState x:Name="Normal"/>
<VisualState x:Name="MouseOver"/>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="EnabledBorder">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="ReadOnly">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="EnabledBorder">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="FocusStates">
<VisualState x:Name="Focused">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="EnabledBorder">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxEditBackgroundBrush}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="EnabledBorder">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxEditBorderBrush}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Unfocused"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<VisualStateManager.CustomVisualStateManager>
<ec:ExtendedVisualStateManager/>
</VisualStateManager.CustomVisualStateManager>
<Border x:Name="EnabledBorder" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Margin="{StaticResource PhoneTouchTargetOverhang}">
<ContentControl x:Name="ContentElement" BorderThickness="0" HorizontalContentAlignment="Stretch" Margin="{StaticResource PhoneTextBoxInnerMargin}" Padding="{TemplateBinding Padding}" VerticalContentAlignment="Stretch"/>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Application.Resources>

It's an old topic, but instead of change template (that does not work for me, I dont't see the other textbox with Blend) you can add boolean to check if the event already did the function or not.

boolean already = false;
private void Tweet_SizeChanged(object sender, EventArgs e)
{
if (!already)
{
already = true;
...
}
else
{
already = false;
}
}

I'm aware that is NOT the perfect way, but I think it's the simple way to do that. And it works.

Nice! I found this question by searching for a related problem and also found this annoying thing in my code. Double event eats more CPU resources in my case. So, I fixed my real-time filter textbox with this solution:

private string filterText = String.Empty;


private void SearchBoxUpdated( object sender, TextChangedEventArgs e )
{
if ( filterText != filterTextBox.Text )
{
// one call per change
filterText = filterTextBox.Text;
...
}


}