2015年2月28日

ASP.NET __doPostBack は jQuery.submit(handler) 未対応

jQuery.submit(handler) でイベントを登録しても、__doPostBack によるサブミット(ポストバック)では呼び出してくれない。
原因は、jQuery.submit(handler) は基本的にイベントリスナ(addEventListener, attachEvent)を使用し、__doPostBack はイベントリスナを考慮してないためである。

[一般的な __doPostBack の実装]

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();
    }
}

DOM の onsubmit プロパティは考慮してくれているが、イベントリスナについてはノータッチである。
(ざっと調べた感じ、イベントリスナのみの呼び出しはかなり面倒・・・というかできるか怪しい)

__doPostBack を使わないようにすれば問題ないのだが、asp:LinkButton や各種コントロールのイベント(SelectedIndexChanged など)を使ってしまうと、__doPostBack の使用は回避できない。

解決手段① onsubmit プロパティを使用

__doPostBack の実装に合わせて、素直に onsubmit プロパティを使ってやれば問題ない。
ただし、複数イベントの登録が面倒になる。
(onsubmit は function を 1 つしか登録できない)

解決手段② __doPostBack 上書き

javascript は関数の上書きができるので、__doPostBack を jQuery.submit(handler) を考慮する関数で上書きする。

[__doPostBack 上書き]

if( typeof __doPostBack === 'function' && typeof theForm === 'object' ){
    var __doPostBackOriginal = __doPostBack;
    __doPostBack = function(eventTarget, eventArgument){
        if( $(theForm).triggerHandler('submit') !== false ){
            // イベントが登録されてない場合は undefined が返ってくるので厳密比較
            // イベントで false を返せば、サブミットは実行されない
            __doPostBackOriginal(eventTarget, eventArgument);
        }
    };
}

注意事項として、上記は event.preventDefault には対応してない。
event オブジェクトはクロスブラウザ対応が面倒なので・・・

また、aspx は <form runat=server> をページ内に 1 つしか定義できないため、theForm が複数あるなどの考慮は不要。

解決手段③ form.submit 上書き

上記と似ているが、今度は大元である form.submit を「サブミットボタン押下」の挙動に変更する。
form.submit では登録したイベントは発生しないが、HTMLElement.click を使ってボタン押下をシミュレートすると、イベントを発生させることができる。

[form.submit 上書き]

// 全 form 対象
[].forEach.call(document.forms, function(f){
    f.submitOriginal = f.submit;
    f.submit = function(){
        // 非表示のサブミットボタンを追加 -> 押下 -> 削除
        $('<input type="submit" name="_適当な重複しにくい名前_" style="display:none" />')
        .appendTo(f).click().remove();
    };
});

注意事項として、onsubmit プロパティを使用している場合は、__doPostBack を使うとイベントが 2 回呼ばれてしまう。
jQuery.submit() でサブミットした場合も、jQuery で登録したイベントが 2 回呼ばれてしまう。
なので、これらは使わないようにするしかない。

副作用は大きいが、form.submit でイベントが発生して欲しい場合は、便利かもしれない。
逆に、イベントが発生しないサブミットは、form.submitOriginal を使用する。

2015年2月22日

table カラムの colspan と nth-child の相性問題

この記事のテーブルレイアウトで苦労したのでメモ。

例えば、列数 > 2 のテーブルの先頭 2 列を色指定したい場合、nth-child を使うと簡単にできる。
[CSS]

/* td と th の先頭 2 列を指定 */
td:nth-child(-n+2), th:nth-child(-n+2) {
    background-color: purple;
}

しかし、先頭 2 列に colspan=2 が含まれる場合・・・
[HTML]

<table>
    <tr>
        <td>1</td>
        <td>2</td>
        <td>3</td>
    </tr>
    <tr>
        <td colspan=2>1-2</td>
        <td>3</td>
    </tr>
</table>

nth-child は colspan を考慮してくれないため、破綻する。
[プレビュー]
123
1-23

解決手段

まず、<colgroup> と <col> を使ってスタイルを指定する方法がある。
◇cssでセル幅を指定するとcolspanで破綻する: 万象酔歩 にくわしく説明されているので、ここでは割愛。

基本的には上記でいいのだが、<td> や <th> に既にスタイルを割り当てている場合、<colgroup> が使えない。
<colgroup> と <col> では、 !important を指定しても <td> と <th> のスタイルを上書きできない。

もう 1 つの方法として、非表示の <td> <th> を利用して、nth-child を適用する方法がある。
[HTML]

<table>
    <tr>
        <td>1</td>
        <td>2</td>
        <td>3</td>
    </tr>
    <tr>
        <td colspan=2>1-2</td>
        <td style="display:none"><!-- 非表示カラム --></td>
        <td>3</td>
    </tr>
</table>
[プレビュー]
123
1-23

非表示要素を使うのは微妙だが、対象のカラムすべてに class を書いたりするよりはましである。

その他

Selectors Level 4 の Column combinator が実装されれば、こんな工夫はいらなくなるはず。

2015年2月11日

プロパティのアクセシビリティと PropertyInfo

PropertyInfo を取得する際、アクセシビリティで混乱したので調査。

プロパティ定義 GetProperties の引数 BindingFlags PropertyInfo 情報
get
アクセサ
set
アクセサ
CanRead CanWrite GetMethod
.Attributes
SetMethod
.Attributes
public public Public Public Public
public private Public Public Private
private public Public Private Public
public - Public Public -
- public Public - Public
protected protected NonPublic Family Family
internal internal NonPublic Assembly Assembly
protected
internal
protected
internal
NonPublic FamORAssem FamORAssem
private private NonPublic Private Private
private - NonPublic Private -
- private NonPublic - Private
※GetMethod と SetMethod の MethodBase.Attributes は、Public/Private/Family/Assembly/FamORAssem
のみ抽出

[調査コード]

class Program
{
    static void Main(string[] args)
    {
        ShowPropInfo(new Props());
    }
    static void ShowPropInfo<T>(T source)
    {
        var t = typeof(T);
        Console.WriteLine("[Public]");
        ShowPropDetail(t.GetProperties(BindingFlags.Instance | BindingFlags.Public));

        Console.WriteLine();

        Console.WriteLine("[NonPublic]");
        ShowPropDetail(t.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic));
    }
    static void ShowPropDetail(IEnumerable<PropertyInfo> props)
    {
        foreach( var prop in props ){
            Console.WriteLine("Name:{0} CanRead:{1} CanWrite:{2}",
                prop.Name, prop.CanRead, prop.CanWrite);

            if( prop.GetMethod != null ) Console.WriteLine(
                "  <Get> Name:{0} Attributes:[{1}]",
                prop.GetMethod.Name, prop.GetMethod.Attributes);

            if( prop.SetMethod != null ) Console.WriteLine(
                "  <Set> Name:{0} Attributes:[{1}]",
                prop.SetMethod.Name, prop.SetMethod.Attributes);
        }
    }
}

class Props
{
    public int plGetSet { get; set; }
    public int plGetpvSet { get; private set; }
    public int pvGetplSet { private get; set; }
    public int plGet { get { return 0; } }
    public int plSet { set { } }

    protected int ptGetSet { get; set; }
    internal int inGetSet { get; set; }
    protected internal int ptinGetSet { get; set; }

    private int pvGetSet { get; set; }
    private int pvGet { get { return 0;} }
    private int pvSet { set { } }
}

まとめ

  • get アクセサ/set アクセサのいずれかが public → BindingFlags.Public の対象
  • 上記以外 → BindingFlags.NonPublic の対象

  • get アクセサ無し → CanRead : False → GetMethod : null
  • set アクセサ無し → CanWrite : False → SetMethod : null

2015年2月10日

VB.NET Action デリゲート型に Function ラムダ式を代入

タイトル通りなのだが、VB.NET では下記のようなコードがコンパイル&実行可能。

Module TestDelegate
    Sub Main()
        Dim act1 As Action = Function() True
        act1()

        ' 引数があるデリゲートも
        Dim act2 As Action(Of Integer) = Function(i) i + 1
        act2(0)
        ' ラムダ式の引数を省略可能
        act2 = Function() True
        act2(0)
        act2 = Sub() Console.WriteLine("void")
        act2(0)
    End Sub
End Module

'[実行結果]
'> void

「厳密でないデリゲート変換」というもので、ラムダ式が導入された時 (Visual Studio 2008) からあった模様。

「Option Strict On」でも適用されるため、「タイプセーフ?なにそれ?おいしいの?」のような機能・・・
前の投稿とかぶるが、これを無効化する Option はあった方がいい。

ちなみに、自分がやってしまったバグ

Module BugExample
    Sub Main()
        Dim elements As New List(Of Element)
        ' 中略
        elements.Where(適当な条件).ForEach(Function(elem) elem.Enabled = True)
    End Sub
End Module

Class Element
    Public Property Enabled As Boolean
    ' 以下略
End Class
※ ForEach は Interactive Extensions の拡張メソッド。
Sub と Function を間違えたのだが、まったく意味が違ってくる。
代入と比較の記号が同じであるため、コンパイルが通り、テストするまで気付けなかった。