WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

全角数字を半角に変換

by WebSurfer 2016年1月15日 17:58

全角 / 半角の変換には Microsoft.VisualBasic 名前空間の Strings.StrConv メソッドがよく使われているようですが、それは使わないで、全角数字を半角に変換する方法を書きます。

基本的には Regex.Replace メソッド (String, MatchEvaluator) を使って文字列の中の全角数字の部分を半角数字に置き換えるのですが、問題は引数の MatchEvaluator に設定するカスタムメソッドをどのように作るかです。

@IT の記事「全角英数字のみを半角に変換するには?」にその例がありますが、そこでは Strings.StrConv メソッドが使われています。

しかしながら、Strings.StrConv メソッドはローケルの違いとか XP 互換モードで期待した結果にならないという問題があるそうです。

数字だけなら辞書を作るのは簡単ですので、Strings.StrConv メソッドを使う代わりに、IEnumerable.Select 拡張メソッドと辞書を使ってカスタムメソッドを実装してみました。

以下にコード例を書きます。その中の Replacer というのが MatchEvaluator に設定するカスタムメソッドです。

Replacer のコードの中で、m.Value はマッチした全角数字の文字列(String 型。IEnumerable<char> を継承している)になります。Select 拡張メソッドは辞書を使って全角数字の文字列の各文字 (Char 型)を半角に置き換え、それを IEnumerable<char> 型のオブジェクトとして返します。

半角数字に置き換えられた IEnumerable<char> 型のオブジェクトを ToArray メソッドで Char[] に変換し、String(Char[]) コンストラクタで半角数字の文字列(String 型)を生成して Replacer メソッドの戻り値として返しています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace ConsoleApplication
{
  class Program
  {
    // 辞書を作る
    public static Dictionary<char, char> dictionary =
      new Dictionary<char, char>() {
        {'0','0'},{'1','1'},{'2','2'},{'3','3'},
        {'4','4'},{'5','5'},{'6','6'},{'7','7'},
        {'8','8'},{'9','9'}
      };
        
    public static string Convert(string source)
    {
      Regex regex = new Regex("[0-9]+");
      return regex.Replace(source, Replacer);
    }

    public static String Replacer(Match m)
    {
      return new String(
        m.Value.Select(n => dictionary[n]).ToArray());
    }

    static void Main(string[] args)
    {
      String source = "0123456789Ab1C02dEfあい36";
            
      Console.WriteLine(source);
      Console.WriteLine(Convert(source));

      // Regex を使わなくても以下のようにして可能
      String replaced = new String(
        source.Select(
          n => (dictionary.ContainsKey(n) ? dictionary[n] : n)
          ).ToArray()
        );
      Console.WriteLine(replaced);
            
      /* 結果は:
      0123456789Ab1C02dEfあい36
      0123456789Ab1C02dEfあい36
      0123456789Ab1C02dEfあい36
      */
    }
  }
}

Tags: ,

.NET Framework

ポストバック前後でスクロール位置維持

by WebSurfer 2016年1月11日 14:11

ASP.NET Web Forms アプリには MaintainScrollPositionOnPostBack プロパティというものがあって、ポストバック前後で上下左右のスクロール位置を維持できる仕組みが用意されています。

ただし、ASP.NET 2.0, 3.0, 3.5 でブラウザに Safari, Chrome を使用した場合は動きません。理由は、先の記事「Safari は downlevel browser?」に書きましたように、Safari, Chrome のブラウザ定義に問題があるからです。(ASP.NET 4 は問題なし)

以下に、上下左右のスクロール位置を維持する仕組み、ASP.NET 2.0, 3.0, 3.5 での Safari, Chrome のブラウザ定義は何故ダメか、その対処方法を書きます。

まずどういう仕組みでスクロール位置を維持するかですが、Page で MaintainScrollPositionOnPostback が true に設定されていると、ブラウザ定義に問題がなければ以下のようなスクリプトと隠しフィールドが html ソースに含まれてレンダリングされます。(注:初期画面では隠しフィールドの value は "0" になり、下の方のインラインスクリプトで window.onload での操作を行う 2 行は含まれません)

<script type="text/javascript">
//<![CDATA[
var theForm = document.forms['form1'];
if (!theForm) {
    theForm = document.form1;
}
function __doPostBack(eventTarget, eventArgument) {
    if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
        theForm.__EVENTTARGET.value = eventTarget;
        theForm.__EVENTARGUMENT.value = eventArgument;
        theForm.submit();
    }
}
//]]>
</script>


<script src="/WebResource.axd?d=Dgk..." type="text/javascript">
</script>


<input type="hidden" 
  name="__SCROLLPOSITIONX" 
  id="__SCROLLPOSITIONX" 
  value="50" />
<input type="hidden" 
  name="__SCROLLPOSITIONY" 
  id="__SCROLLPOSITIONY" 
  value="200" />


<script type="text/javascript">
//<![CDATA[
theForm.oldSubmit = theForm.submit;
theForm.submit = WebForm_SaveScrollPositionSubmit;

theForm.oldOnSubmit = theForm.onsubmit;
theForm.onsubmit = WebForm_SaveScrollPositionOnSubmit;

theForm.oldOnLoad = window.onload;
window.onload = WebForm_RestoreScrollPosition;
//]]>
</script>

上のコードで真中あたりにある WebResource.axd は HTTP ハンドラで、Page の埋め込みリソースとして設定されている外部スクリプトファイルを取得します。

その外部スクリプトファイルの中に、上にアップしたコードの下の方にあるインラインスクリプトで使う WebForm_SaveScrollPositionSubmit メソッド他の定義が含まれています。(興味があれば開いて中身を見てください。Visual Studio でデバッグ実行すれば開いてブレークポイントを設定したりできます)

これらのスクリプトによって、(1) ポストバックされる前の画面のスクロール位置を取得、(2) 隠しフィールドの value に画面のスクロール位置を設定してサーバーに送信、(3) サーバーから応答が戻ってきて画面が再描画されるときに隠しフィールドからポストバック前のスクロール位置を取得、(4) 画面のスクロール位置をポストバック前と同じに設定・・・という操作を行います。

しかしながら、ブラウザ定義ファイルで capability 要素 (capabilities の子要素) の supportsMaintainScrollPositionOnPostback が true に設定されてないと ASP.NET は必要なスクリプトや隠しフィールドを生成しません。

ASP.NET 2.0, 3.0, 3.5 用のブラウザ定義ファイルは以下のフォルダにあります。

C:\Windows\Microsoft.NET\Framework\v2.0.50727\CONFIG\Browsers

2016/1/11 時点では mozilla.browser ファイルの中に id="Safari1Plus" という browser 要素があって、Safari(かなり古いものは除く)および Chrome はその定義の適用を受けます(継承元の Default ← Mozilla ← Gecko ← Safari の定義も)。

Default のブラウザ定義で supportsMaintainScrollPositionOnPostback は false に設定されていますが、Default ← Mozilla ← Gecko ← Safari ← Safari1Plus という継承の過程でそれを true に設定するものがありません。(注:ASP.NET 4 のブラウザ定義ファイルは問題ありません)

結果、Page で MaintainScrollPositionOnPostBack プロパティを true に設定しても ASP.NET は必要なスクリプトや隠しフィールドを生成しません。

解決策は、アプリケーションルート直下に App_Browser フォルダを追加し、さらにそのフォルダに .browser ファイル(名前は任意)を追加し、その中で refID 属性を使用して既存の id="Safari1Plus" のブラウザー定義で supportsMaintainScrollPositionOnPostback が true になるように設定してやります。

具体的には、以下のコードを新たに作った .browser ファイルに記述します。これによって、Safari, Chrome でも必要なスクリプトや隠しフィールドが生成されるようになるはずです。

<browsers>
  <browser refID="Safari1Plus">
    <capabilities>
      <capability 
        name="supportsMaintainScrollPositionOnPostback"
        value="true" />
    </capabilities>
  </browser>
</browsers>

CONFIG\Browsers フォルダの中の mozilla.browser 他の定義済みのブラウザ定義ファイルを修正するのは NG ですので注意してください。修正しても修正結果は反映されませんので。(詳しくは MSDN Blog の記事「ASP.NET の IE10 対応について」を見てください)

Tags:

ASP.NET

進捗状況の表示

by WebSurfer 2015年12月28日 18:07

ASP.NET Web Forms アプリで、サーバー側にて非同期で行われている処置の進捗状況を Label コントロールや jQuery UI の Progressbar を使って表示する方法を書きます。

進捗状況の表示

ScriptManager, UpdatePanel, UpdateProgress を使えば非同期要求をかけて応答が戻ってくるまでの間、サーバーで処理中という情報をユーザーに示すことはできます。具体的な方法は MDSN ライブラリの記事UpdateProgress コントロールの概要を見てください。

しかしながらサーバー側での処置が何 % 終わったかというような進捗状況を表示することはできません。何故ならサーバー側での処理が終わるまでサーバーからは何の応答も返ってこないからです。

サーバー側での処理の進捗状況をブラウザに表示する場合、サーバー側での処理は別スレッドで行うようにし、ブラウザからタイマーを使って定期的に AJAX で進捗状況をサーバーに問合せ、サーバーからその応答をもらって表示するということは可能です。

時間のかかる処理をこのような方法で行うのが適切か、Web ファーム / ガーデン対応やワーカープロセスのリサイクル対応はどうするかという話はちょっと置いておいて、こうすれば何とかできるという具体的な例を以下に書きます。

(Thread を使うのは "don't even think about it" という意見もありますので注意してください。詳しくはこの記事の下の方の 2015/12/29 追記を見てください)

まず「時間のかかる処理」ですが、処置を行うだけでは当然ダメで、何らかの方法で進捗状況を取得できる必要があります。(進捗状況が取得できないような処理もあるかと思いますが、そういう場合はお手上げです)

具体例は以下のコードを見て下さい。DoTask メソッドで処置を行うと同時に進捗(以下の例では 0 ~ 100 まで)を記録し、Progress プロパティで進捗状況を取得できるようにしています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Threading;

public class Task
{
    public int Progress { get; set; }
    
    public Task()
    {
        this.Progress = 0;
    }
    
    public void DoTask()
    {
        for (int i = 0; i < 100; i++)
        {
            Thread.Sleep(100);
            this.Progress = i + 1;
        }
    }    
}

ASP.NET Web Forms アプリのページで上のコードの処理を別スレッドで実行します。Timer を使って 1 秒毎に非同期要求をかけ、Progress プロパティで進捗(上のコード例では 0 ~ 100 まで)を取得し、Label コントロールと jQuery UI の Progressbar に表示するようにしています。

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Threading" %>

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

<script runat="server">
  // Task は App_Code に定義したカスタムクラス
  // その中の DoTask メソッドで処理を行う
  // 進捗状況は Progress プロパティで取得する
  private Task task = null;

  protected void Page_Load(object sender, EventArgs e)
  {
    // Session["Task"] が null でなければ処理中
    object obj = Session["Task"];
    if (obj != null)
    {
      task = (Task)obj;
    }
  }

  protected void Button1_Click(object sender, EventArgs e)
  {
    // 処理中の場合は何もしないで return
    if (task != null)
    {
      return;
    }

    // Task クラスを初期化して Session に保持
    task = new Task();
    Session["Task"] = task;
        
    // 別スレッドで処理を実行
    Thread newThread = new Thread(task.DoTask);
    newThread.Start();
  }

  // Timer.Interval プロパティに設定したインターバル
  // (この例では 1 秒)で非同期呼び出しがかかって
  // Page_Load メソッドのあとこのメソッドが実行される
  protected void Timer1_Tick(object sender, EventArgs e)
  {
    if (task != null)
    {
      if (task.Progress == 100)
      {
        Label1.Text = "完了";
                
        // ラベルに表示しない場合もこれだけは必要
        Session.Remove("Task");
      }
      else
      {
        Label1.Text = task.Progress.ToString() + "%";
      }

      // Progress Bar に表示する進捗データを設定
      if (ScriptManager.GetCurrent(this).IsInAsyncPostBack)
      {
        ScriptManager.GetCurrent(this).
          RegisterDataItem(this, task.Progress.ToString());
      }
    }
    else
    {
      Label1.Text = "処置は行われていません";
    }        
  }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title></title>
  <script src="/jquery.js" type="text/javascript"></script>
  <script src="/jquery-ui.js" type="text/javascript"></script>
  <link href="/jquery-ui.css" rel="stylesheet" type="text/css" />
  <style type="text/css">
    .ui-progressbar {
        position: relative;
    }

    .progress-label {
        position: absolute;
        left: 50%;
        top: 4px;
        font-weight: bold;
        text-shadow: 1px 1px 0 #fff;
    }
  </style>
  <script type="text/javascript">
  //<![CDATA[

    function pageLoad(sender, args) {
      if (args.get_isPartialLoad() === false) {
        var manager = 
          Sys.WebForms.PageRequestManager.getInstance();
        manager.add_endRequest(OnEndRequest);
      }
    }

    // Timer で非同期要求がかかり応答が戻ってくるたびに以下の
    // メソッドが実行される
    function OnEndRequest(sender, args) {
      // サーバー側の Timer1_Tick メソッドで設定した進捗
      // データをクライアント側で取得
      var progress = args.get_dataItems()["__Page"];

      // 進捗データを Progress Bar に表示する
      if (progress) {
        $("#progressbar").
          progressbar("value", Number(progress));
      }
    }

    // jQuery UI のデモのコードを借用
    $(function () {
      var progressbar = $("#progressbar"),
          progressLabel = $(".progress-label");

      progressbar.progressbar({
        value: false,
        change: function () {
          progressLabel.
            text(progressbar.progressbar("value") + "%");
        },
        complete: function () {
          progressLabel.text("Complete!");
        }
      });
    });
  //]]>
  </script>
</head>
<body>
  <form id="form1" runat="server">
  <asp:ScriptManager ID="ScriptManager1" runat="server">
  </asp:ScriptManager>

  <%--これが ProgressBar になる
  jQuery UI のデモのコードを借用--%>
  <div id="progressbar">
    <div class="progress-label">Loading...</div>
  </div>

  <asp:Button ID="Button1" runat="server" 
    Text="Start Task" OnClick="Button1_Click" />

  <asp:UpdatePanel ID="UpdatePanel1" runat="server">
    <ContentTemplate>
      <asp:Label ID="Label1" runat="server" />
    </ContentTemplate>
    <Triggers>
      <asp:AsyncPostBackTrigger ControlID="Timer1" 
        EventName="Tick" />
    </Triggers>
  </asp:UpdatePanel>

  <asp:Timer ID="Timer1" runat="server" 
    Interval="1000" OnTick="Timer1_Tick">
  </asp:Timer>
  </form>
</body>
</html>

上記のコードを Chrome から呼んで、[Start Task]ボタンクリックで処置を開始し、その進捗状況を表示したのが上の画像です。

-------- 2015/12/29 追記 --------

MSDN Blog の記事 Performing Asynchronous Work, or Tasks, in ASP.NET Applications によると、Thread を使うのは、その記事を書いた Microsoft の開発者よりはるかにスマートに実装できるのでなければ "don't even think about it" だそうです。(汗)

理由はその記事の FAQ の 4 番目に書いてありますが、以下に概略を書いておきます(誤訳はあるかも)。

  1. CLR ThreadPool を使用するのに比べて非常にコストが高い。
  2. 自分で作った Thread に I/O 要求が残ってないか終了前にチェックしなければならない。
  3. システムのパフォーマンスを保つには実行されている Thread の数が適切でなければならないが、自分で Thread を作るのであればパフォーマンスを保つのは自分の責任になる。

暇なサイトならともかく、要求が多いサイトの場合はやはり問題になりそうです。

Tags:

AJAX

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  2017年1月  >>
25262728293031
1234567
891011121314
15161718192021
22232425262728
2930311234

View posts in large calendar