WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

DataGrid, GridView に動的に列を追加 (2)

by WebSurfer 21. September 2021 14:56

昔「DataGrid, GridView に動的に列を追加」という記事を書きましたが、その記事よりスマートにできる方法を見つけたので以下に書いておきます。

GridView での実行結果

先の記事ではどのようにしたかと言うと、DataGrid, GridView を構成している TableCellCollection の適切な位置に TableCell を追加するというプリミティブなやり方です。なので、列を追加するだけでもかなりコードを書かなければならないし、コントロールにデーターバインドしようとするとさらに面倒なことになります。

ところが最近になって Microsoft の Visual Studio 2008 用の MSDN ライブラリに「方法 : DataList Web サーバー コントロールのテンプレートを動的に作成する 」という記事があるのを見つけ、その方法が DataGrid, GridView にも使えることを知りました。

その MSDN ライブラリはもうネットには見つかりませんので、スクリーンショットを撮ってこの記事の下の方に貼っておきます。興味がありましたら見てください。

先の記事に書いたような TableCell を追加してなんちゃらという面倒なことをする必要はなく、TemplateColumn (DataGrid の場合) または TemplateField (GridView の場合) を生成し、それを DataGrid.Columns.Add メソッドまたは GridView.Columns.Add で追加するということで列の追加が可能です。

TemplateColumn, TemplateField の中身はユーザーコントロール (.ascx) から作成します。作成したユーザーコントロールから TemplateControl.LoadTemplate メソッドで ITemplate インターフェイスのインスタンスを生成し、当該テンプレート (ItemTemplate, AlternatingItemTemplate 等) に代入してやります。

ユーザーコントロール内に配置したコントロールには <%# Eval("Name") %> というようなデータバインド式を含めることができます。そうしておけば、静的にデータバインド式を使った場合と同様に、自動的にデータソースからデータを取得して表示されます。

具体例は以下の通りです。上の画像を表示したコードで、データソースに SQL Server のサンプルデータベース Northwind の Products テーブルを使って、その PtoductName 列と Discontinued 列を動的に追加した TemplateColumn, TemplateField に表示しています。

(1) ユーザーコントロールの作成

テンプレートの中身のコントロールを配置したユーザーコントロール (.ascx) を作成します。以下の例では CheckBox と Label をテンプレートの中身として配置しています。データバインド式を含めているところにも注目してください。

<%@ Control Language="C#" AutoEventWireup="true" 
    CodeBehind="NewTemplate.ascx.cs" 
    Inherits="WebApplication2.NewTemplate" %>

製造中止: 
<asp:CheckBox ID="CheckBox1" runat="server" 
    Checked='<%# Eval("Discontinued") %>' 
    Enabled="false" />
/ 
製品名: 
<asp:Label ID="Label1" runat="server" 
    Text='<%# Eval("ProductName") %>'>
</asp:Label>

ユーザーコントロールの名前は、ここでは NewTemplate.ascx としました。名前は任意に設定して構いませんが .aspx.cs の LoadTemplate メソッドの引数にその名前を使いますので注意してください。

(2) TemplateColumn, TemplateField の追加

.aspx.cs

DataGrid は TemplateColumn を、GridView は TemplateField を使うという違いがあり、それによる若干の違いがありますので注意してください。

using System;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace WebApplication2
{
    public partial class LoadTemplate : System.Web.UI.Page
    {
        protected void Page_Init(object sender, EventArgs e)
        {
            // DataGrid に TemplateColumn を追加
            var templateColumn = new TemplateColumn();
            templateColumn.HeaderText = "動的に追加した TemplateColumn";
            templateColumn.ItemTemplate = LoadTemplate("NewTemplate.ascx");
            DataGrid1.Columns.Add(templateColumn);

            // GridView に TemplateField を追加
            var templateField = new TemplateField();
            templateField.HeaderText = "動的に追加した TemplateField";
            templateField.ItemTemplate = LoadTemplate("NewTemplate.ascx");
            GridView1.Columns.Add(templateField);
        }
    }
}

.aspx

マスターページを使っています。ほぼ 100% デザイナで自動生成したコードで、DB の ProductID のみを表示するように作成しました。ProductName と Discontinued は上の .aspx.cs で動的に追加した列に表示します。

<%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" 
    AutoEventWireup="true" CodeBehind="LoadTemplate.aspx.cs" 
    Inherits="WebApplication2.LoadTemplate" %>

<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
    
    <asp:SqlDataSource ID="SqlDataSource1" 
        runat="server" 
        ConnectionString="<%$ ConnectionStrings:NORTHWINDConnectionString %>" 
        SelectCommand="SELECT TOP 10 [ProductID], [ProductName], [Discontinued] 
                       FROM [Products]">
    </asp:SqlDataSource>

    <h3>DataGrid</h3>
    <asp:DataGrid ID="DataGrid1"
        runat="server" 
        AutoGenerateColumns="False" 
        DataSourceID="SqlDataSource1">
        <Columns>
            <asp:BoundColumn DataField="ProductID" 
                HeaderText="ProductID" />
        </Columns>
    </asp:DataGrid>

    <h3>GridView</h3>
    <asp:GridView ID="GridView1" 
        runat="server" AutoGenerateColumns="False" 
        DataKeyNames="ProductID" 
        DataSourceID="SqlDataSource1">
        <Columns>
            <asp:BoundField DataField="ProductID" 
                HeaderText="ProductID" 
                InsertVisible="False" ReadOnly="True" 
                SortExpression="ProductID" />
        </Columns>
    </asp:GridView>
</asp:Content>

最後に、上にも書きましたが、参考にした Visual Stidio 2008 の MSDN ライブラリのスクリーンショットを下に貼っておきます。

MSDN ライブラリ

Tags: , , ,

ASP.NET

Entity Framework で ROW_NUMBER

by WebSurfer 20. September 2021 15:27

SQL Server のテーブルからレコードを抽出する際、あるフィールドに ORDER BY 句を適用して並べ替え、その順序で連番を振りたいという場合は ROW_NUMBER (Transact-SQL) を使うことができます。

ROW_NUMBER の使用

上の画像がその例で、ProductName フィールドに ORDER BY 句を適用して昇順に並べ替えて、ROW_NUMBER を使ってその順で 1 から連番を振って、その連番を SeqNum という名前で取得しています。期待通り SeqNum に連番が取得されているのが分かるでしょうか?

それを同じことを Entity Framework ではどのようしたらできるかというのがこの記事の話です。

この記事を書いた時点で自分が調べた限りですが、ROW_NUMBER は Entity Framework ではサポートされてないようで(NuGet から Thinktecture.EntityFrameworkCore.SqlServer をインストールして使えるようにするという手はあるようですが)、Linq to Entities / Objects のクエリ式やメソッド式��� ROW_NUMBER を含めることはできないようです。

Microsoft のドキュメント「生 SQL クエリ」に紹介されているように、FromSqlRaw 拡張メソッドを使用して上の画像の SELECT クエリをそのまま SQL Server に投げるという手も考えましたが、戻ってきた結果から SeqNum を取得する方法が見つかりません。(自分が見つけられないだけで手はあるのかもしれませんが)

ではどうするかですが、ググって調べていると Generating Sequence Numbers In LINQ Query という記事を見つけました。

IEnumerable<T> インターフェイスの Select 拡張メソッドのオーバーロードの一つに Select<TSource,TResult>(IEnumerable<TSource>, Func<TSource,Int32,TResult>) があって、それを使えば 0 番から始まる連番の index を取得することができるというものです。

その記事を参考に、上の画像の ROW_NUMBER を使った SELECT クエリと同じ結果を Linq to Entities / Objects を使って取得するコードを書いてみました。以下の通りです。

public List<ProductWithSeqNum> GetListWithSeqNum()
{
    var data = _context.Products
               .Select(a => new 
               {
                   ProductId = a.ProductId,
                   ProductName = a.ProductName,
                   UnitPrice = a.UnitPrice.Value
               });

    var list = data.AsEnumerable()
               .OrderBy(a => a.ProductName)
               .Select((a, index) => new ProductWithSeqNum
               {
                   SeqNum = index + 1,
                   ProductId = a.ProductId,
                   ProductName = a.ProductName,
                   UnitPrice = a.UnitPrice
               });

    return list.ToList();
}

まず、Linq to Entities を使って Products テーブルから ProductId, ProductName, UnitPrice 抽出して IQueryable<匿名クラス> のオブジェクトを取得し、変数 data に格納しています。

次に、data.AsEnumerable() で IEnumerable<匿名クラス> に変換して、それを OrderBy メソッドで ProductName 順に並べ替え、上に述べた Select メソッドの index を使って 1 から始まる連番を SeqNum に格納しています。

ProductWithSeqNum クラスを Data Transfer Object (DTO) として使っていて、結果を List<ProductWithSeqNum> オブジェクトとして戻しています。ProductWithSeqNum クラスの定義は以下の通りです。

public class ProductWithSeqNum
{
    public int SeqNum { get; set; }

    public int ProductId { get; set; }

    public string ProductName { get; set; }

    public decimal UnitPrice { get; set; }
}

上の GetListWithSeqNum メソッドで取得した結果をコンソールに書き出すと以下の通りとなります。一番上の画像と同様な結果が取得できているのが分かるでしょうか。

結果をコンソールに書き出し

Tags: , ,

ADO.NET

Linq to Entities でのキャッシュに注意

by WebSurfer 19. September 2021 16:54

Linq to Entities でエンティティを追加したり取得したりする場合、デフォルトではエンティティは DbContext にキャッシュされるそうです。それでハマったので、再びそういうことがないよう備忘録を書いておくことにしました。

検証結果のテーブル

元の話は Teratail のスレッド「[C#,.NET5,EFCore5+Microsfot.Data.Sqlite] トランザクションのロールバックが意図通りに動かない」です。その話は表題とは異なり、ロールバックは期待通り動いていたがキャッシュのために動いてないと勘違いしたというものです。

本題に入る前に、まずトランザクション / ロールバックの話を書いておきます。

トランザクションは、保留中の状態 (BeginTransaction の呼び出し後、Commit の呼び出し前) だけからロールバックできるようになっています。逆に言えば、保留中の状態であれば RollBack を呼び出せばロールバックできます。

Microsoft のドキュメント「トランザクションの使用」の「トランザクションを制御する」のセクションのサンプルコードを見てください。

SaveChanges はすべて完全に成功していても、transaction は SaveChanges でコミットされるわけではない(保留中の状態にある)ので、transaction.Commit(); がないと transaction が Dispose される時にロールバックされます。

そのコードで transaction.Commit(); ⇒ transaction.RollBack(); としたのが Teratail のスレッドの話ですが、その場合はもちろん無条件でロールバックされます。

DB はロールバックはされたのですが、キャッシュされたエンティティまではロールバックされないので、キャッシュされたエンティティを使ってその後の操作を行った結果ロールバックが失敗しているように見えたという話です。

エンティティをキャッシュする理由は、Microsoft のドキュメント「追跡と追跡なしのクエリ」に書いてあるように、追跡を行うためということです。どういうことかと言うと、エンティティに加えられた変更を追跡していって、SaveChanges メソッドで変更結果を DB に反映するということらしいです。

その記事に書いてある "If EF Core finds an existing entity, then the same instance is returned. EF Core won't overwrite current and original values of the entity's properties in the entry with the database values." というのは「エンティティがキャッシュにあればキャッシュから取得する。DB の値で上書きされることはない」と言っているように思えます。

"If the entity isn't found in the context, then EF Core will create a new entity instance and attach it to the context." というのは「context.Blogs.Add(...) というようにするとそのエンティティも DbContext にキャッシュされる」ということのように思えます。

Teratail のスレッドのように、自分で RollBack と書くようなことはしないはずなので、普通はキャッシュによる問題には遭遇しなそうな気がします。

そこを、若干無理やりですが、こんなことをすると問題になるかもしれないと作ったサンプルが下のコードです。Visual Studio 2019 のテンプレートで作った .NET 5.0 のコンソールアプリです。DB は SQLite を使っています。

Teratail のものとは違って、普通に Commit と書いて例外発生時のみロールバックするようにしています。ただし、例外を catch してなかったことにしているので、その後キャッシュされたエンティティを使っての作業が継続できるというものです。

これを実行した結果の DB の内容が上の画像です。Name が SEQ1, SEQ2, SEQ3 の既存のレコードがあって、それに赤枠で囲った SEQ5 のレコードを追加しています。

transaction で囲った 1 つ目の SaveChanges で SEQ5 の Value を 2 に UPDATE していますが、2 つ目の SaveChanges で PK 制約違反の例外が発生するので Commit できずロールバックされるようになっています。DB 上では上の画像の通りロールバックされて SEQ5 の Value は初期値 1 のままになっています。

コードのコメント「ここでエンティティがキャッシュされる」のところで SEQ5 のエンティティがキャッシュされています。ロールバックされた後、変数 seq1, seq2, seq3 に SEQ5 のエンティティを取得してその Value プロパティをコンソールに書き出すと以下のように順に 2, 2, 1 となります。

コンソールへの出力

seq1, seq2 はキャッシュから取得されており、キャッシュはロールバックされないので、それらの Value はコードで書き換えた 2 のままになっています。

予想外だったのは seq2 です。これは要注意だと思いました。context.Sequences.ToListAsync() で DB に SELECT クエリを発行してロールバック後のすべてのレコードを取得してくるのですが、SEQ5 のエンティティのみはキャッシュで書き換えられてしまっています。

seq3 は Reload して DB からデータを取得してキャッシュを書き換えた結果です。上の画像のロールバック後の DB の値が Reload で取得されて Value は 1 になっています。

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ConsoleAppSQLite
{
    class Program
    {
        static async Task Main(string[] args)
        {
            using (var context = new TestDBContext())
            {
                // ここでエンティティがキャッシュされる
                context.Sequences.Add(
                    new SequenceValue { Name = "SEQ5", Value = 1 });

                // 上で作った Name = "SEQ5", Value = 1 の新規データを INSERT
                context.SaveChanges();

                try
                {
                    using (var transaction = context.Database.BeginTransaction())
                    {
                        // 上で INSERT した Name = "SEQ5", Value = 1 のレコード
                        // の Value を 2 にして UPDATE
                        SequenceValue seq = context.Sequences.Find("SEQ5");
                        seq.Value = 2;                        
                        context.SaveChanges();

                        // Name = "SEQ2", Value = 1 のレコードを INSERT
                        // Name は主キーで "SEQ2" のレコードは DB に存在する
                        // ので PK 制約違反
                        context.Sequences.Add(
                            new SequenceValue { Name = "SEQ2", Value = 100 });
                        context.SaveChanges();

                        // 上の SaveChanges で例外が発生するので Commit されない
                        // 結果 transaction が Dispose される際ロールバックされる
                        transaction.Commit();
                    }
                }
                catch (Exception)
                {
                    // 例外処理・・・何もしないと例外はなかったことになる
                }

                // キャッシュから取得する。ロールバックはキャッシュは書き換え
                // ないので、上のコードで seq.Value = 2 とした結果が取得される
                SequenceValue seq1 = context.Sequences.Find("SEQ5");

                // キャッシュから取得しないようにするには AsNoTracking() を
                // 追加して以下のようにする
                //SequenceValue seq1 = await context.Sequences
                //                           .AsNoTracking()
                //                           .SingleAsync(x => x.Name == "SEQ5");

                Console.WriteLine(seq1.Value);

                // DB に SELECT クエリを発行して全てレコードを取得してくるが
                // Name(主キー)が "SEQ5" のエンティティだけはキャッシュから
                // 取得して list を書き換える。                
                List<SequenceValue> list = await context.Sequences.ToListAsync();

                // 書き換えられないようにするには AsNoTracking() を追加して
                // 以下のようにする
                //List<SequenceValue> list = 
                //    await context.Sequences.AsNoTracking().ToListAsync();

                SequenceValue seq2 = list.Find(x => x.Name == "SEQ5");
                Console.WriteLine(seq2.Value);

                // Reload すると DB からデータを取得してキャッシュを書き換
                // えるので Value はロールバック後の値 1 になる
                SequenceValue seq3 = context.Sequences.Find("SEQ5");
                await context.Entry(seq3).ReloadAsync();
                Console.WriteLine(seq3.Value);
            }
        }
    }


    [Table("SEQUENCES")]
    public class SequenceValue
    {
        [Key, Column("NAME")] 
        public string Name { get; set; }
        
        [Column("VALUE")] 
        public int Value { get; set; }
    }

    public class TestDBContext : DbContext
    {
        protected override void OnConfiguring(DbContextOptionsBuilder builder)
        {
            var path = @"C:\Users\...省略...\test.db";
            var connStr = "Data Source=" + path;

            builder.UseSqlite(connStr);

            // 出力ウィンドウに EF Core ログを表示
            builder.LogTo(msg => System.Diagnostics.Debug.WriteLine(msg));
        }
        public DbSet<SequenceValue> Sequences { get; set; }
    }
}

あと、コメントにも書きましたが、DbExtensions.AsNoTracking メソッドを適用すると "Returns a new query where the entities returned will not be cached in the DbContext or ObjectContext." ということで、キャッシュは使われなくなるようです。こういうやり方が正解なのかどうかは分かりませんが。

Tags: , , , ,

ADO.NET

About this blog

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

Calendar

<<  September 2021  >>
MoTuWeThFrSaSu
303112345
6789101112
13141516171819
20212223242526
27282930123
45678910

View posts in large calendar