вторник, 25 января 2011 г.

Влияние свойства DefaultButton у ASP.Net Panel на обработку события onKeyPress дочернего TextBox'а (Internet Explorer)

Вчера и сегодня убил довольно много времени на поиск и устранение одного неприятного бага.

Есть панель, на ней текстбоксы. Есть javascript, который обрабатывает пользовательский ввод в текстбоксы, позволяя вводить только числа. То есть подписываемся на onKeyPress и возвращаем false, если юзер ввёл неразрешённый символ. При этом событие отменяется и символ не вводится.
Всё работало хорошо до какого-то момента, а потом работать перестало. Проблема была в Internet Explorer, в других целевых браузерах (Opera, Firefox, Chrome) косяк не проявлялся.



После долгих раскопок выяснил, что javascript-обработка сломалась после того, как стало использоваться свойство DefaultButton у родительской панели.
По сути, разрабатывалось что-то типа визарда (естественно, намного сложнее и наворочаннее, чем в примере, который тут приводится). Есть несколько шагов-"диалоговых окон", каждый из которых содержит некие элементы ввода/отображения данных и одну или несколько командных кнопок - "да", "нет", "далее" и т. п. Захотелось, чтобы, если пользователь, закончив ввод данных в текстбоксы, нажмёт <Enter>, происходили бы действия, соответствующие щелчку на дефолтной кнопке, например "далее". В ASP.Net есть решение "из коробки" - свойство DefaultButton у панели. Надо просто присвоить ему идентификатор нужной кнопки.
Так вот, после того, как это было сделано, перестало отменяться событие onKeyPress у текстбокса (обработчик вызывается, отрабатывает но возврат false при вводе запрещённого символа ни к чему не приводит - символ вводится).
Связано это, по-видимому, с тем, что родительская панель с заданным свойством DefaultButton сама обрабатывает onKeyPress, отлавливая нажатие Enter. Проблема, напомню, имеет место только в IE.

Возможные решения
1) Подписываться не на onKeyPress, а на onKeyDown. Этот вариант плох тем, что в event.keyCode будет не код символа, а код клавиши. Обработка получается более кривой, поскольку, например, при вводе "3" и "#" (Shift + 3) генерится один и тот же код, потому что клавиша одна, аналогично одинаковый код генерится при вводе русской точки и английского слеша, которые тоже на одной клавише (какой символ вводится - зависит от текущей раскладки клавиатуры). Я сначала пошёл по этому пути, но так и не добился требуемого результата, хотя, может быть, это и возможно.
2) В обработчике onKeyPress запретить событию всплывать, присвоив event.cancelBubble значение true. Это я и сделал в моём случае. Надо только не забыть проверить, какой символ введён, и, если это 13-ый символ (<Enter>), ничего не делать, позволить всплытие, чтобы наша панель с установленным свойством DefaultButton могла перехватить это событие и "щёлкнуть" на дефолтной кнопке.
if (code == 13)
  return;
 else
  event.cancelBubble = true;
3) Ну и ещё один путь - отказаться от использования свойства DefaultButton панели и реализовывать этот функционал самостоятельно (ловить нажатия <Enter> и делать что нужно в этом случае).

Неоценимую помощь осознании причин проблемы оказал вот этот пост (англ.), найденный с помощью небезызвестной поисковой системы.

Простой примерчик, для иллюстрации.
Здесь злополучное свойство DefaultButton устанавливается программно у панели pWizard - ему присваивается идентификатор одной из кнопок, находящихся на показываемой в текущий момент вьюшке (см. CodeBehind). Каждая вьюшка - шаг визарда.

<%@ Page Language="C#" AutoEventWireup="true"
  CodeBehind="BugSample.aspx.cs"
  Inherits="Amikko.BugSample" %>

<!DOCTYPE html PUBLIC
  "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
 <title>Title</title>
 <script type="text/javascript" language="javascript">
  function UnsignedIntValidator(sender, e) {
   var code = e.keyCode;

   // Решение проблемы с onkeypress.
   // Если не <Enter>, то запрещаем "всплытие" события.
   if (code == 13)
    return;
   else
    e.cancelBubble = true;

   return code >= 48 && code <= 57
  }
 </script>
</head>
<body>
 <form id="form1" runat="server">
  <asp:ScriptManager ID="scriptManager" runat="server"
    EnableScriptGlobalization="true" />

   <asp:UpdatePanel runat="server" ID="upWizard"
    RenderMode="Inline" UpdateMode="Conditional">
    <ContentTemplate>
     <asp:Panel ID="pWizard" runat="server">
      <asp:MultiView ID="mvSteps" runat="server">
       <asp:View ID="vViewFirst" runat="server">
        <div>
         Вьюшка vViewFirst<br />
         Текстбокс, позволяющий вводить только числа<br />
         <asp:TextBox ID="tbNumber" runat="server"
          onkeypress="return UnsignedIntValidator(this, event ? event : window.event);" />
        </div>
        <asp:Button ID="bButton1" runat="server"
           Text="Кнопка 1" />&nbsp;
        <asp:Button ID="bButton2" runat="server"
           Text="Кнопка 2" />&nbsp;
        <asp:Button ID="bDefault" runat="server"
           Text="Кнопка по умолчанию" 
         onclick="bDefault_Click" />
       </asp:View>
       <asp:View ID="vViewSecond" runat="server">
        <div>
         Вьюшка vViewSecond<br />
         Какие-то контролы...<br />
         <asp:TextBox ID="TextBox1" runat="server" /><br />
         <asp:CheckBox ID="CheckBox1" runat="server" />
        </div>
        <asp:Button ID="bBack" runat="server"
           Text="< Назад" onclick="bBack_Click" />
       </asp:View>
      </asp:MultiView>
     </asp:Panel>
    </ContentTemplate>
   </asp:UpdatePanel>
 </form>
</body>
</html>

CodeBehind:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace Amikko
{
 public partial class BugSample : System.Web.UI.Page
 {
  /// <summary>
  /// Шаги
  /// </summary>
  private enum Steps
  {
   /// <summary>
   /// Соответствует вьюшке vViewFirst
   /// </summary>
   First = 0,

   /// <summary>
   /// Соответствует вьюшке vViewSecond
   /// </summary>
   Second = 1,
  }

  /// <summary>
  /// Определяет идентификатор кнопки,
  /// нажатие на которую должно быть вызвано при
  /// нажатии клавиши Enter
  /// </summary>
  /// <returns>Идентификатор дефолтной кнопки</returns>
  private string GetDefaultButtonForCurrentStep()
  {
   var step = (Steps)mvSteps.ActiveViewIndex;
   switch (step)
   {
    case Steps.First:
     return bDefault.ID;
    case Steps.Second:
     return bBack.ID;
    default:
     return string.Empty;
   }
  }

  private void InitDefaultButton()
  {
   pWizard.DefaultButton =
     GetDefaultButtonForCurrentStep();
   pWizard.Focus();
  }

  /// <summary>
  /// Переход к другому шагу
  /// Отображается соответствующая View
  /// </summary>
  /// <param name="newStep">Шаг создания заказа.</param>
  private void ChangeStepTo(Steps newStep)
  {
   mvSteps.ActiveViewIndex = (int)newStep;
   InitDefaultButton();
  }

  protected void Page_Load(object sender, EventArgs e)
  {
   ChangeStepTo(Steps.First);
  }

  protected void bDefault_Click
    (object sender, EventArgs e)
  {
   ChangeStepTo(Steps.Second);
  }

  protected void bBack_Click(object sender, EventArgs e)
  {
   ChangeStepTo(Steps.First);
  }
 }
}

Комментариев нет:

Отправить комментарий