WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

日付と通貨の書式設定

by WebSurfer 4. February 2019 18:10

ASP.NET MVC5 アプリで日付(DateTime 型)と通貨(Decimal 型)を表示し、編集してデータベースを更新する場合、どのように書式設定を行うのが良いかということについて書きます。

日付と通貨の書式設定

上の画像は DisplayFor と TextBoxFor を上下に並べて表示していますが、その「契約日」と「価格」に示したように、

  1. DisplayFor 等を使った表示の場合のみ書式設定を行い、
  2. テキストボックスに値を表示する場合は書式設定しない。または、"{0:F0}" のような書式設定に留めパースできない文字は含めないようにする。

・・・のがよさそうです。

スキャフォールディング機能を使って CRUD 可能なアプリを作ると Index (レコード一覧), Create, Delete, Edit, Details というアクションメソッドとそれに対応するビューが生成されますが、Index, Delete, Details を上記 (1) で、Create, Edit を上記 (2) で表示するということです。

テキストボックスに表示する文字列も 2019年2月4日 とか ¥1,000 のように表示することはできますが、クライアント側での検証を無効にしてサーバーに POST したとしても、DateTime 型や Decimal 型にパースできないので結局モデルバインディングの際の検証が通りません。

どうしてもテキストボックスに表示する文字列も 2019年2月4日 とか ¥1,000 のようにしたいということであれば、モデルのプロパティも含めて全て String 型として扱い、検証は正規表現のみを使って行うということも考えられますが、データベースの型が datetime とか money でしょうから、サーバー側での取り扱いを考えると現実的ではなさそうです。

それに、ユーザーとしても、編集する際にいちいち ¥ とか , を入力するのは煩わしいはずで、喜んではくれないのでは?

上の画像のように表示するための方法ですが、DisplayFor での表示の書式はモデルのプロパティに DisplayFormatAttribute を付与して設定し、テキストボックスには TextBoxFor を使ってその第 2 引数に "{0:yyyy/MM/dd}"、"{0:F0}" などの書式を設定するのがよさそうです。

EditorFor を使うと書式が自由に設定できないし(DisplayFormatAttribute の設定と同じでよければ可能ですが)、レンダリングされる html ソースの input 要素の type 属性が問題になることがありますので使う場合は要注意です。

具体的には以下のようにします。

Model

public class PurchaseRecord
{
  [Display(Name = "ID")]
  [Required(ErrorMessage = "{0} は必須")]
  [RegularExpression(@"^\d{1,5}$", 
      ErrorMessage = "数字 1 ~ 5 文字")]
  public int ID { get; set; }

  [Display(Name = "契約日")]
  [Required(ErrorMessage = "{0} は必須")]
  [RegularExpression(
      @"^\d{4}/\d{2}/\d{2}( \d{1,2}:\d{2}:\d{2})?$", 
      ErrorMessage = "yyyy/MM/dd 形式")]
  [DisplayFormat(DataFormatString = "{0:yyyy年M月d日}")]
  public DateTime ContractDate { get; set; }

  [Display(Name = "価格")]
  [Required(ErrorMessage = "{0} は必須")]
  [RegularExpression(@"^\d{1,5}$", 
      ErrorMessage = "数字 1 ~ 5 文字")]
  [DisplayFormat(DataFormatString = "{0:C0}")]
  public decimal Price { get; set; }
}

View(一部の抜粋)

<div class="form-group">
  @Html.LabelFor(model => model.ID, 
    htmlAttributes: new { @class = "control-label col-md-2" })
  <div class="col-md-10">
    @Html.DisplayFor(model => model.ID)
    @Html.EditorFor(model => model.ID, 
      new { htmlAttributes = new { @class = "form-control" } })
    @Html.ValidationMessageFor(model => model.ID, 
      "", new { @class = "text-danger" })
  </div>
</div>

<div class="form-group">
  @Html.LabelFor(model => model.ContractDate, 
    htmlAttributes: new { @class = "control-label col-md-2" })
  <div class="col-md-10">
    @Html.DisplayFor(model => model.ContractDate)
    @Html.TextBoxFor(model => model.ContractDate, 
      "{0:yyyy/MM/dd}", new { @class = "form-control" })
    @Html.ValidationMessageFor(model => model.ContractDate, 
      "", new { @class = "text-danger" })
  </div>
</div>

<div class="form-group">
  @Html.LabelFor(model => model.Price, 
    htmlAttributes: new { @class = "control-label col-md-2" })
  <div class="col-md-10">
    @Html.DisplayFor(model => model.Price)
    @Html.TextBoxFor(model => model.Price, 
      "{0:F0}",new { @class = "form-control" })
    @Html.ValidationMessageFor(model => model.Price, 
      "", new { @class = "text-danger" })
  </div>
</div>

あと、オマケの話ですが、ASP.NET Web Forms アプリで使う GridView などで書式設定する際も同様なことはできます。以下の画像を見てください。

GridView で日付と通貨の書式設定

赤枠で囲った部分は[編集]ボタンをクリックして編集モードにしたテキストボックスですが、他の行との違いを見てください。

Tags: , ,

MVC

VB.NET の ByVal / ByRef と値型 / 参照型

by WebSurfer 20. January 2019 12:37

Visual Basic .NET (VB.NET) のメソッドの引数の値渡し (ByVal) / 参照渡し (ByRef) と、変数の値型 / 参照型をそれらの引数に渡す場合の違いについて、このように考えると理解しやすいと思っていることを書きます。

ちなみに、C# では ByVal に相当するキーワードがなくデフォルトで値渡しになるだけで、考え方は同じです。(ByRef に相当する ref というキーワードはあります)

まず、メソッドの引数に付与する ByVal, ByRef の違いですが、メソッドの呼び出し元から引数に値を渡す時、以下のようしていると考えれば良いと思います。

  • ByVal: 別の入れ物に中身をコピーして渡す
  • ByRef: 入れ物を中身ごと渡す

上記で「入れ物」というのは、例えば、Dim X As Integer で宣言された変数 X で、「中身」というのは X = 10 のようにして変数 X に代入された値 10 のことです。

次に、変数の値型・参照型ですが、クラスや配列は参照型、Integer のような構造体は値型になります。詳しくは、3-2 値型と参照型を見てください。

メソッドに ByVal, ByRef どちらで渡そうと、変数の参照型が値型に変わったり、値型が参照型に変わることはありません。

という訳で、以下の 4 つのケースに分けて考えると良いと思います。

(1) 値型を ByVal で渡す

呼び出し元の入れ物の中身を、メソッド側で用意した別の入れ物に、コピーして渡す。

呼び出し元とメソッドで入れ物が異なるので、メソッド側で入れ物の中身をどのように操作しようと呼び出し元の変数には何の影響もない。

(2) 値型を ByRef で渡す

呼び出し元の入れ物とメソッドに渡された入れ物は同じなので、メソッド側で中身を変更すれば呼び出し側でも中身が変更される。

(3) 参照型を ByVal で渡す

参照型なので渡すのはオブジェクト(インスタンス)を指すアドレス情報。ByVal なのでメソッド側で用意した別の入れ物にアドレス情報をコピーして渡している。

つまり、呼び出し元の変数とメソッド側の変数の中身は同じアドレス情報即ち単一のオブジェクトを指している。

なので、メソッド側で変数が指すオブジェクトを変更すると、呼び出し元の変数が指すオブジェクトでも同じ結果となる。

(4) 参照型を ByRef で渡す

基本は上記 (3) と同じ。

ただし、呼び出し側の入れ物とメソッドに渡された入れ物は同じなので、メソッド側で新しくオブジェクト(インスタンス)を生成してそれのアドレス情報を入れ物に代入すると、呼び出し元の入れ物の中身が指すオブジェクトも、メソッド側で生成した新しいオブジェクトになる。

Tags: , , ,

.NET Framework

ASP.NET MVC 用の CompareValidator

by WebSurfer 31. December 2018 12:06

ASP.NET Web Forms には CompareValidator という検証コントロールがあって、2 つの TextBox に入力された値の大小比較をして検証できる機能があります。

それと同様な検証機能を ASP.NET MVC で実装する方法を書きます。(元の話は Teratail のスレッド ASP.NET MVC4 TimeSpan型入力値の検証です)

MVC の CompareValidator

ASP.NET MVC には CompareAttribute という検証用のデータアノテーション属性がありますが、それには大小を比較する機能はなく、同じか否かを検証するのみです。

なので、例えば、ユーザーが開始時間と終了時間をテキストボックスに入力した際に、終了時間が開始時間より後になっていることを検証するような場合は、カスタム検証機能を実装することになります。

複数のプロパティにまたがった検証をする場合、Teratail のスレッド「MVC モデルのバリデーションについて」の回答に書いたように、CustomValidationAttribute を使うか、モデルに IValidatableObject インターフェイスを実装するという手段を使うのが普通のようです。

しかし、2 つのテキストボックス入力の大小比較程度なら、CustomValidationAttribute を使わなくても、リフレクションを使って比較対象のプロパティの値を取得し、それと自身のプロパティの値を比較して検証結果を返すということができます。

参考にしたのは Compare Validator in MVC という記事の Limitations of the Compare DataAnnotation 以下のセクションです。

上の画像を表示するのに使ったサンプルコードは以下の通りです。TimeSpan 型のプロパティを検証の対象としていますが、DataTime 型その他でもキャストを変更して同様に検証可能です。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
using System.Globalization;

namespace Mvc5App.Models
{
  public class KintaiModel
  {
    public IList<Record> Records { get; set; }
  }

  public class Record
  {
    public int Id { set; get; }
    public string Week { set; get; }
    public TimeSpan Open { set; get; }

    [TimeGreaterThan("Open", 
                   "{0} は Open より後に設定してください")]
    public TimeSpan Close { set; get; }

    // ・・・中略・・・
  }

  [AttributeUsage(AttributeTargets.Property)]
  public class TimeGreaterThanAttribute : ValidationAttribute
  {
    private string _startTimePropertyName;

    // コンストラクタ
    public TimeGreaterThanAttribute(
            string startTimePropertyName, string errorMsg)
    {
      this._startTimePropertyName = startTimePropertyName;
      this.ErrorMessage = errorMsg;
    }

    public override string FormatErrorMessage(string name)
    {
      return string.Format(CultureInfo.CurrentCulture,
                           ErrorMessageString, name);
    }

    protected override ValidationResult IsValid (
        object value, ValidationContext validationContent)
    {
      // ObjectType はこの属性を付与したプロパティが属する
      // クラス、即ち上のコードの Record クラスとなる
      System.Reflection.PropertyInfo propertyInfo = 
          validationContent.ObjectType.
          GetProperty(this._startTimePropertyName);
      object propertyValue = 
          propertyInfo.
          GetValue(validationContent.ObjectInstance, null);

      if ((TimeSpan)value > (TimeSpan)propertyValue)
      {
        return ValidationResult.Success;
      }
      else
      {
        // ValidationResult の引数に null または "" を設定する
        // と FormatErrorMessage メソッドの戻り値が設定される
        return new ValidationResult(null);
      }
    }
  }
}

上の画像・コードではクラスのコレクションのモデルバインディングを行っていますが、そのような場合でもリフレクションを使って比較対象のクラスのプロパティを特定して検証可能です。ただ、リフレクションを使うということで、性能的にどうかという懸念は消せてませんが。

また、上のサンプルコードはサーバー側だけの検証で、クライアント側での JavaScript / jQuery による検証はかかりません。

単一のプロパティのカスタム検証であれば、@IT の記事の「第4回 検証属性の自作とクラス・レベルのモデル検証」のセクションに書かれているようにして、サーバー側とクライアント側両方の検証機能を実装できます。

しかし、今回のケースのように 2 つのプロパティにまたがった検証をする場合、スクリプトを ASP.NET MVC の検証システムの中に無理なく不整合なく取り込む具体例は少なくとも自分は見たことがないです。(自分が知らないだけだという可能性は否定しきれませんが)

Tags: ,

Validation

About this blog

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

Calendar

<<  August 2019  >>
MoTuWeThFrSaSu
2930311234
567891011
12131415161718
19202122232425
2627282930311
2345678

View posts in large calendar