てなわけで、AutoCompleteExtenderの中身をちょっと弄って他の付随情報も扱えるようにしてみた。具体的には図1の様に郵便番号を3桁以上入力するとAutoCompleteが働いて、対応する住所まで引っ張ってきて反映(図2)させる様にする。ちなみにWeb Programmingはそこそこ長いが、ASP.NETは駆け出しなんで、どこかで間違っている可能性もあります。
で、いきなりデザイナでポトペタかと思いきや、先に補完候補を返すWebサービスクラスを作る。通常のAutoCompleteExtenderなら1次元の文字列配列を返すが、ここでは、郵便番号と住所を保持するクラスを定義して、そのオブジェクト配列を返すようにしている。別にわざわざクラスを書いて返さなくても、文字列の多次元配列でもいいのだが、いつものノリで書いてる。ちょっと長いがPostalComplete.cs(PostalComplete.asmx)
using System;
using System.Web;
using System.Collections.Generic;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Data;
using System.Data.Common;
using System.Text.RegularExpressions;
using System.Web.Script.Services;
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ScriptService]
public class PostalComplete : System.Web.Services.WebService
{
public PostalComplete()
{
}
public class ZipAddress
{
private string _zip;
private string _address;
public string zip
{
get { return this._zip; }
}
public string address
{
get { return this._address; }
}
public ZipAddress(string zip, string address)
{
this._zip = zip;
this._address = address;
}
}
[WebMethod]
public ZipAddress[] GetPostalCompleteList(String prefixText, int count)
{
string _prefixText = prefixText.Trim().Replace("-", "");
Regex reg = new Regex("^[0-9]{3,7}$");
if (!reg.IsMatch(_prefixText)) return null;
DbConnection con = DbProviderFactories.GetFactory(DbProviderFactories.GetFactoryClasses().Rows[0]).CreateConnection();
con.ConnectionString = System.Configuration.ConfigurationManager.ConnectionStrings[0].ConnectionString;
con.Open();
DbCommand cmd = con.CreateCommand();
cmd.CommandText = "SELECT * FROM tb_postal_code WHERE zip_code LIKE :C1 ORDER BY zip_code";
DbParameter dpC1 = cmd.CreateParameter();
dpC1.DbType = DbType.String;
dpC1.Value = _prefixText + "%";
dpC1.ParameterName = "C1";
cmd.Parameters.Add(dpC1);
DbDataReader dr = cmd.ExecuteReader();
List<ZipAddress> list = new List<ZipAddress>();
int c = 1;
while (dr.Read())
{
list.Add(new ZipAddress((string)dr["zip_code"], (string)dr["city_name"] + (string)dr["local_name"]));
if (++c > count) break;
}
con.Close();
return list.ToArray();
}
}

図3.フォームレイアウト

図4.AutoCompleteExtenderプロパティ

図5.テキストボックスプロパティ
入力された文字列(prefixText)を元にデータベースに問い合わせ、その結果の必要行分(count)をオブジェクト配列にして返している。ちなみにtb_postal_codeテーブルのzip_codeが郵便番号、city_nameが市区町村名、local_nameが町域名になってます。今回都道府県名はなし。SQLはLIMITクエリかCURSORを使ったほうが、DB→ASP.NET間の転送が少なくて済むのでしょうけど、どちらもDB依存度が高そうで...ここら辺りはデータプロバイダの方で面倒見てほしいなぁ。
ようやくデザイナでレイアウト。と言っても住所を反映させるテキストボックスが多い程度で、他はよくあるサンプルと同じ(図3)。郵便番号のボックスIDがZip、住所はAddressとしています。関連付ける方法も標準と同じだが、AutoCompleteExtenderでややこしいのが、その設定プロパティがAutoCompleteExtenderのプロパティ(図4)よりターゲットとなるテキストボックスのプロパティ(図5)の方が多い、つかこっちにAutoCompleteExtender関連の設定が出てくる。
さて、残るはAutoCompleteExtender本体と言うべきAutoCompleteBehavior.jsの改造。改造と言ってもAutoCompleteExtenderを置いた時点でAutoCompleteBehavior.jsが読み込まれているようになっているから、そのあとに関連するメソッドを上書きで置き換える。置き換えるメソッドは非同期通信の結果(PostalComplete/GetPostalCompleteListの返り値)を受け取るコールバックの_updateメソッドとテキストボックスに選択値を入れる_setTextメソッド。_setTextメソッドは引数に文字列だけを受け取るようになっているので、エレメントオブジェクトを受け取るように修正するのだが、当然呼び出し側もすべて修正する必要が出てくる。_onListMouseDownと_onKeyDownメソッドがそれにあたります。予想以上に結構増えた...orz。以下に置き換えるメソッドのスクリプトを挙げるが、加える場所はaspxファイルの下の方、AutoCompleteBehavior.jsを読み込む位置より後になっていなくてはならない。
<script type="text/javascript">
AjaxControlToolkit.AutoCompleteBehavior.prototype._update = function(prefixText, completionItems, cacheResults) {
if (cacheResults && this.get_enableCaching()) {
if (!this._cache) {
this._cache = {};
}
this._cache[prefixText] = completionItems;
}
this._completionListElement.innerHTML = '';
this._selectIndex = -1;
if (completionItems && completionItems.length) {
for (var i = 0; i < completionItems.length; i++) {
var itemElement = document.createElement('div');
itemElement.innerHTML=completionItems[i].zip+'<br />'+completionItems[i].address; //修正
itemElement.__item = completionItems[i]; //修正
var itemElementStyle = itemElement.style;
itemElementStyle.padding = '1px';
itemElementStyle.textAlign = 'left';
itemElementStyle.textOverflow = 'ellipsis';
itemElementStyle.backgroundColor = this._textBackground;
itemElementStyle.color = this._textColor;
itemElementStyle.fontSize = '9pt'; //追加
this._completionListElement.appendChild(itemElement);
}
var elementBounds = CommonToolkitScripts.getBounds(this.get_element());
this._completionListElement.style.width = '200px'; //修正
this._popupBehavior.show();
}
else {
this._popupBehavior.hide();
}
}
AjaxControlToolkit.AutoCompleteBehavior.prototype._setText= function(ev) {
this._timer.set_enabled(false);
this._currentPrefix = ev.__item.zip; //修正
var element = this.get_element();
var control = element.control;
// todo: should check for 'derives from' too and should somehow manually cause TB to raise property changed event
if (control && control.set_text) {
control.set_text(ev.__item.zip); //修正
}
else {
element.value = ev.__item.zip; //修正
}
$get('Address').value = ev.__item.address; //追加
this._hideCompletionList();
}
AjaxControlToolkit.AutoCompleteBehavior.prototype._onListMouseDown= function(ev) {
if (ev.target !== this._completionListElement) {
this._setText(ev.target); //修正
}
}
AjaxControlToolkit.AutoCompleteBehavior.prototype._onKeyDown= function(ev) {
var k = ev.keyCode ? ev.keyCode : ev.rawEvent.keyCode;
if (k === Sys.UI.Key.esc) {
this._hideCompletionList();
ev.preventDefault();
}
else if (k === Sys.UI.Key.up) {
if (this._selectIndex > 0) {
this._selectIndex--;
this._highlightItem(this._completionListElement.childNodes[this._selectIndex]);
ev.preventDefault();
}
}
else if (k === Sys.UI.Key.down) {
if (this._selectIndex < (this._completionListElement.childNodes.length - 1)) {
this._selectIndex++;
this._highlightItem(this._completionListElement.childNodes[this._selectIndex]);
ev.preventDefault();
}
}
else if (k === Sys.UI.Key.enter) {
if (this._selectIndex !== -1) {
this._setText(this._completionListElement.childNodes[this._selectIndex]); //修正
ev.preventDefault();
}
}
else if (k === Sys.UI.Key.tab) {
if (this._selectIndex !== -1) {
this._setText(this._completionListElement.childNodes[this._selectIndex]); //修正
}
}
else {
this._timer.set_enabled(true);
}
}
</script>
長々となってしまいました。郵便番号で補完候補10件だけだとあまり価値がないし、ポップアップされる候補ウィンドーはある程度の大きさでそれ以上はスクロールさせるとか、いろいろツッコミどころはありますが、あくまで手始めのサンプルと言うことで。つーより、やっぱりカスタムコントロール化してJavaScriptを書くのを極力避ける様にする必要があると思う。
Author: rakko