kazuk は null に触れてしまった

C# / .NET 系技術ネタ縛りでお送りしております

月別アーカイブ: 12月 2010

ASP.NET 4.0 で Menu コントロールのレンダリングのされ方


asp:Menu でおなじみ、 ASP.NET 2.0 では「死んじゃえばいいのに」扱いを私にされたメニューコントロールのレンダリングについて。

無駄に head に style を吐くのは

IncludeStyleBlock=”False”

で抑止できるようになりました。パチパチパチ

Menu.IncludeStyleBlock プロパティ (System.Web.UI.WebControls)

なんでもテーブルというテーブル厨だった ASP.NET 2.0 に対して ul/li でのリストレンダリングもできるようになりました。

<asp:Menu
ID=”Menu” runat=”server”
DataSourceID=”SiteMapDataSource1″
StaticDisplayLevels=”5″ IncludeStyleBlock=”False”
CssClass=”menu” RenderingMode=”List” >
<LevelMenuItemStyles>
</LevelMenuItemStyles>
</asp:Menu>

こんなマークアップでのHTML出力は以下のようになります。

<a href=”#Menu_SkipLink”>

<img alt=”ナビゲーション リンクのスキップ”

    src=”/WebResource.axd?d=むにゃむにゃ” width=”0″ height=”0″

    style=”border-width:0px;” /></a>

<div class=”menu” id=”Menu”>

<ul class=”level1″>

<li><a class=”level1″ href=”url”>title</a></li>

<li><a class=”level2″ href=”url”>title</a></li>

<li><a class=”level3 selected” href=”url”>title</a></li>

<li><a class=”level4″ href=”url”>title</a></li>

<li><a class=”level4″ href=”url”>title</a></li>

</ul>

</div>

<a id=”Menu_SkipLink”></a>

本体につけた CssClass は div に適用され、レベルは class で識別されます。 ってなわけで、 ul/li のネストに頼った CSS だと当然に段がつかねーになります。ってことで、今日の僕ははまってました。どうやら ul/li のネストで出すオプションはない模様です。

さらには div で囲むのを抑止する方法もなく…

Menu_SkipLink を抑止するとかどっかオプションあるんじゃないかな。ナイってこと無いよね!

つーわけで、結局コントロールのレンダリングが融通聞かなくてどっかから持ってきたCSSが当たらなくてウガーってなる伝統は守られております。(残念)

まぁ、こんな時は普段の自分なら即 ControlAdapter 書くんですが、今日はHTML書きモード(というか文書書き前のフォーマット決め)なのでコードは書かない!を通してどんな HTMLでるのかなー、じゃーCSSはこうだねーとかなり優しく応対してみました。

というわけで、久々に薄い話でした!

VsPackage プロジェクトのデフォルト値の罠


Visual Studio SDK (download)を入れてると Visual Studio 内で動かせる VsPackage を作れるようになりまする。 (プロジェクトの作成時には言語下の Extensibility より)

image

んで、この VsPackage なんですが、最初に作られる時のエディション選択が Professional ノミになってますって事で要注意。

image

(さて、本来 AddOn をサポートしない Express Editions がリストされているアレ。msi とか作って無理やり捻じ込むと入るって事でしょうかねー)

Ultimate とか Premium を使ってるのは怖い人が多いんで、バンバン叩かれない様に注意しましょー。

スクリーンショット二つだけという簡単な blog 、先週土曜にやった生産技術勉強会でのネタをチロチロと出していくblogがしばらく続きますよって事。

MsBuild 4.0の新機能でビルド時コード生成とか


MsBuild 4.0 には結構良い新機能が入っております。

ここんとこツイッターでモニョモニョ言ってる MsBuild インラインタスクとか。

MsBuild ターゲット

  • RunBeforeTargets および RunAfterTargets (MSBuild 4.0)

(原文ママ、スキーマ見た感じでは BeforeTargets / AfterTargets が正しい)

これを使って TFS ビルド中にバージョンを埋めるタスクを作りましたのでおすそ分け

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build"
         xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <UsingTask
      TaskName="CreateAssemblyVersionSource"
      TaskFactory="CodeTaskFactory"
      AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll" >
    <ParameterGroup>
      <Path ParameterType="System.String" Required="true" />
      <VersionAndRevision ParameterType="System.String" Required="true" />
      <BuildUri ParameterType="System.String" Required="true"  />
    </ParameterGroup>
    <Task>
      <Code Type="Fragment" Language="cs">
<![CDATA[
        string buildNumber = new Uri( BuildUri ).Segments.Last();

        Log.LogMessage( "CreateAssemblyVersionSource Path: {0}",Path );
        StringBuilder sb = new StringBuilder();
        sb.AppendLine( "using System;" );
        sb.AppendLine( "using System.Reflection;" );
        sb.AppendLine( "using System.Runtime.InteropServices;" );
        sb.AppendLine( "using System.Resources;" );
        sb.AppendFormat( "[assembly: AssemblyVersion(\"{0}.{1}\")]"            ,VersionAndRevision, buildNumber ).AppendLine();
        sb.AppendFormat( "[assembly: AssemblyFileVersion(\"{0}.{1}\")]"            ,VersionAndRevision,buildNumber ).AppendLine();
        File.WriteAllText( Path, sb.ToString() );
]]>
      </Code>
    </Task>
  </UsingTask>

  <Target Name="LabelAssembly" Condition="'$(BuildUri)'!=''"
          AfterTargets="PreBuildEvent" BeforeTargets="CoreCompile"  >
    <Message Text="LabelAssembly target running" />

    <CreateAssemblyVersionSource
        Path="AssemblyVersion.cs"
        VersionAndRevision="$(VersionAndRevision)"
        BuildUri="$(BuildUri)" />

    <ItemGroup>
      <Compile Include="AssemblyVersion.cs" />
    </ItemGroup>

  </Target>
</Project>

やっている事としては結構単純で、 File.WriteAllText で cs ファイルを吐きだす事の AssemblyVersion / AssemblyFileVersion アセンブリ属性を。

それを Compile ItemGroup にInclueしてるだけです。

TFSビルド時には BuildUri にビルドの識別Uriがきますので、これがある時だけターゲットを定義するように Condition しており。

AfterTargets/BeforeTargets を使って CoreCompile ターゲットの前に実行される様に設定しています。

AfterTargets/BeforeTargetsによって関連するターゲットには何も変更を入れる必要がなくターゲットを差し込む事ができるというのが素晴らしい所でございます。

<PropertyGroup>

<VersionAndRevision>1.0.0</VersionAndRevision>

</PropertyGroup>

<Import Project=”ターゲットを保存した場所”/>

をcsprojの末尾でやるとTFSでのビルド番号がバージョンに反映されます。

このインラインタスクでやってる StringBuilder での編集をT4でPreprocessorしたテンプレートによって ITask実装しておくと、ビルドシーケンス中でのコード生成はもはやお手の物って訳で、組み合わせ次第で夢が広がりますねーなお話。

MSの中の人には MsBuild 4.0 関連の情報を MSDN Library のメニューにマージして頂きたいとか…。

.NET CLR2 と CLR4 の StringBuilder のパフォーマンス


以下コードにより計測

    class Program
    {
        static void Main(string[] args)
        {
            Random rnd = new Random();
            Console.WriteLine("Hit enter to start");
            Console.ReadLine();
            var stopwatch = Stopwatch.StartNew();
            for (int i = 0; i < 10000; i++)
            {
                // 64KB のテキストをランダム文字で生成
                StringBuilder sb = new StringBuilder();
                while (sb.Length < 65535)
                {
                    sb.Append('a' + rnd.Next(26));
                }
            }
            stopwatch.Stop();
            Console.WriteLine(stopwatch.ElapsedTicks);
            Console.ReadLine();
        }
    }
まずCLR2 での結果

Hit enter to start

891252599

約90秒程度で完了、その時の特にメモリ系の挙動を PerfMon でとったスクリーンショットの注目点を以下。

CLR2 – #Gen1 Collections

20101207111830

CLR2 – #Gen2 Collections

20101207111945

これが全く同一値という世代別GCにとっては悪夢のシチュエーション。

CLR2 – Large Object Heap Size

20101207113019

LOH の上下からLOHのガベージコレクションが走っている事が解る

CLR2 – % Time in GC

20101207113243

LOH の下がるタイミングで %Time in GC にピンがたっている。

続いてCLR4での結果

Hit enter to start

662106546

数値としては 134%、

パフォーマンスカウンタの差異はこんな感じ

CLR4 – #Gen 1 Collections

20101207114226

CLR4 – #Gen 2 Collections

20101207114336

なんと Gen 2コレクションの発生0。

CLR4 – Large Object Heap Size

20101207114445

LOH もべったり。

CLR4 – % Time in GC

20101207114535

#Gen 1 Collectionsの簡易GCで済むので、コストも低い。

結論、個々のシステムの特性によって感応度に違いはあるかも知れないけど、比較的大きな文字列を編集処理してる場合には CLR2 に対してCLR4の StringBuilder は約3割高速で、主因はStringBuilder内部でのバッファ方式の変更による物。

ReSharper による CLR2 のStringBuilder定義表示

20101207115108

m_StringValue に実体が入ってるので、20KBを超えるとLOH行き、しかもvolatileというCPUに優しくない存在。

同じく ReSharper による CLR4 のStringBuilder 定義表示

20101207115832

m_ChunkPrevious によってチャンク化されていて m_ChunkChars に実体が分割して持たれるという構造、分割されたそれぞれは 8000文字まで、要するに16KBなのでLOHには絶対に行かない。

結論

素晴らしい!アプリケーションをCLR2からCLR4にするだけで数十%のパフォーマンス向上も夢じゃないよね。

夏ぐらいから気付いてたんだけど、正確に分析したくなったので測定してみましたなブログ。

MEFでディレクトリカタログを追いかける(C# Advent Calender jp:2010 12/02)


ってタイトルで用意してたんだけど、結果的には追加はいいけど、削除、更新は駄目だったの巻

MEF いいっすよねー。

ロジック殆ど無しのホストに Extensibility で拡張を仕込んでいければ、システムのメンテナンス単位を小さくできる。停止時間を当然に減らせる。

良い事だらけだ!

って感じなんですが、本当に拡張していけるのか試していってという感じ。

Managed Extensibility Framework の概要

MEF では発見可能性、拡張性、および移植性に重点が置かれているのに対し、MAF では拡張機能の特定とアセンブリの読み込みおよびアンロードに重点が置かれています。

InfoQ: CLRが、何回目かの、「最初の」プラグインモデルを採用。

  • System.Addin は、使用されなくなったAppDomainを自動的に、アンロードし、メモリを再利用できるように、管理している。

 

MAF使わないとアンロードできないのか…

そんな事は無い、アセンブリのシャドウコピーがあるじゃないか!

って試したコードが以下。

using System;
using System.Collections.Generic;
using System.IO;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;

namespace MefConsoleApplication
{
    public interface IDoSomething
    {
        void DoSomethig();
    }

    public class Program
    {

#pragma warning disable 649
        [ImportMany(AllowRecomposition = true)]
        private IEnumerable<Lazy<IDoSomething>> _parts;
#pragma warning restore 649

        private CompositionContainer _container;
        private FileSystemWatcher _fsWatcher;
        private DirectoryCatalog _directoryCatalog;

        public void Run()
        {
            AggregateCatalog aggregateCatalog = new AggregateCatalog();
            aggregateCatalog.Catalogs.Add(
                new AssemblyCatalog(typeof(Program).Assembly));
            // ReSharper disable AssignNullToNotNullAttribute
            string extentionPath = Path.Combine(
                Path.GetDirectoryName(typeof(Program).Assembly.Location),
                "..\\..\\Extentions");
            // ReSharper restore AssignNullToNotNullAttribute
            _fsWatcher = new FileSystemWatcher(extentionPath);
            _fsWatcher.Changed += FsWatcherChanged;
            _fsWatcher.EnableRaisingEvents = true;

            _directoryCatalog = new DirectoryCatalog(
                extentionPath);
            _directoryCatalog.Changed += CatalogChanged;
            aggregateCatalog.Catalogs.Add(
                _directoryCatalog);
            aggregateCatalog.Changed += CatalogChanged;
            _container = new CompositionContainer(aggregateCatalog);
            _container.ComposeParts(this);

            foreach (var part in _parts)
            {
                part.Value.DoSomethig();
            }

            while (Console.ReadLine() != "exit")
            {
            }
        }

        void FsWatcherChanged(object sender, FileSystemEventArgs e)
        {
            Console.WriteLine("extentions directory changed");
            _directoryCatalog.Refresh();
        }

        void CatalogChanged(object sender, ComposablePartCatalogChangeEventArgs e)
        {
            Console.WriteLine("catalog changed " + sender.GetType().Name);
            _container.ComposeParts(this);

            foreach (var part in _parts)
            {
                part.Value.DoSomethig();
            }
        }

        static void Main()
        {
            if (!AppDomain.CurrentDomain.ShadowCopyFiles)
            {
                Console.WriteLine("ShadowCopyFilesがオフです");
                AppDomainSetup appDomainSetup = new AppDomainSetup();
                appDomainSetup.ShadowCopyFiles = "true";
                appDomainSetup.ShadowCopyDirectories = Path.GetTempPath();
                appDomainSetup.CachePath = Path.GetTempPath();

                AppDomain execDomain = AppDomain.CreateDomain("execDomain",
                    AppDomain.CurrentDomain.Evidence, appDomainSetup);
                execDomain.ExecuteAssembly(typeof(Program).Assembly.Location);
                return;
            }
            Console.WriteLine("ShadowCopyFilesがオンです");

            Program prog = new Program();
            prog.Run();
        }
    }

    [Export(typeof(IDoSomething))]
    public class ImplSomething1 : IDoSomething
    {
        public void DoSomethig()
        {
            Console.WriteLine("do something 1");
        }
    }
}

 

Main での AppDomainSetup を介して自分を実行し直すって方法で、アプリケーションドメインをシャドウコピー可能にしている。

using System;
using System.ComponentModel.Composition;
using MefConsoleApplication;

namespace ApplicationComponent1
{
    [Export(typeof(IDoSomething))]
    public class Class1 : IDoSomething
    {
        public void DoSomethig()
        {
            Console.WriteLine("extention component1");
        }
    }
}

 

ってアセンブリを放り込むと、ディレクトリの変更を検出して、ちゃんとアセンブリがロードされる。大成功だ。

20101203192208

で、放り込んだもんを削除しようとする。

20101203192308

残念!ってわけで拒否されてしまいました…。

MEF で DirectoryCatalog のChangeを追うには以下が注意事項でございます。

  1. FileSystemWatcherで監視してないと駄目、_directoryCatalog.Refresh()でカタログを更新する必要がある。
  2. [ImportMany(AllowRecomposition = true)] 重要、AllowRecompositionをtrueにしないとRejectされる。
  3. シャドウコピーを有効にしてもアセンブリの削除はできないぜ。

 

って訳で、長い!なMEFでディレクトリカタログを追ってみたけど失敗した!な記事でございます。

C# Advent Calendar jp: 2010 : ATND の 12/2日分って事で次の日の人よろしく。

有用?よくわかんない記事で済まん。

T4でサムネイル画像を生成する


Webの制作っていうと何かと画像処理って出てくるわけでございます。

一括処理とかムニャムニャ考えないと日がな一日ツールをポチポチする羽目になってタンポポ中毒になってしまいます。

てな訳で T4 から GDI+ で一括処理だ!

とりあえずサムネイルを作るだけならこんな感じ。もちろんの事ながら画像を収める img タグとかも同時に吐かせようと思えば余裕!

 

   1:  <#@ template debug="false" hostspecific="true"  language="C#" #>
   2:  <#@ output extension=".txt" #>
   3:  <#@ assembly name="EnvDTE" #>
   4:  <#@ assembly name="System.Core"#>
   5:  <#@ assembly name="System.Drawing" #>
   6:  <#@ import namespace="System.Drawing"#>
   7:  <#@ import namespace="System.Drawing.Drawing2D"#>
   8:  <#@ import namespace="System.Drawing.Imaging"#>
   9:  <#@ import namespace="System.IO"#>
  10:  <#@ import namespace="System.Collections.Generic"#>
  11:  <# 
  12:      var dir= Path.GetDirectoryName( Host.TemplateFile );
  13:      var thumbImageDir = Path.Combine( Path.GetDirectoryName(dir), "thumbs");
  14:      double maxWidth = 320;
  15:   #>
  16:      Source dir:    <#= dir #>
  17:      Target dir: <#= thumbImageDir #>
  18:  <#
  19:   
  20:      foreach( var pngFile in Directory.GetFiles( dir,"*.png" ) )
  21:      {
  22:  #>
  23:  <#=pngFile#> を変換中
  24:  <#
  25:          using( Image img = Image.FromFile( Path.Combine( dir, pngFile ) ) )
  26:          {
  27:              Size sizeImg = img.Size;
  28:              if( sizeImg.Width>maxWidth ) 
  29:              {
  30:                  double factor = maxWidth / (double)sizeImg.Width ;
  31:                  sizeImg.Width = (int) (sizeImg.Width * factor);
  32:                  sizeImg.Height =(int) (sizeImg.Height * factor);
  33:  #>
  34:      original width:<#=img.Width#>
  35:      original height:<#=img.Height#>
  36:      factor: <#=factor#>
  37:      width: <#=sizeImg.Width#>
  38:      height: <#=sizeImg.Height#>
  39:  <#            
  40:              }
  41:              using( Image small = new Bitmap( sizeImg.Width, sizeImg.Height, img.PixelFormat ) )
  42:              {
  43:                  using( Graphics g = Graphics.FromImage( small ) )
  44:                  {
  45:                      g.CompositingQuality = CompositingQuality.HighQuality;
  46:                      g.InterpolationMode =InterpolationMode.HighQualityBicubic;
  47:                      g.DrawImage( img,0,0, sizeImg.Width, sizeImg.Height );
  48:                  }
  49:                  string savePath =Path.Combine(thumbImageDir, Path.GetFileName( pngFile ) );
  50:  #>
  51:      saveTo:<#=savePath#>
  52:  <#
  53:                  small.Save( savePath,ImageFormat.Png );
  54:              }
  55:          }
  56:      }
  57:  #>

 

一応ちゃんとしっかり using を使ってるけど、へくってると画像の大きさとかによってはVSが巻き添えになる可能性もなきにしもあらずなんで、usingはしっかりしようね!

Observable.CombineLatest に渡された要素のどっちが先?


暇ってわけじゃないんだけど、@igetaさんのツイートが琴線に触れたので書いてみた。

xs.CombineLatest(ys, (x, y) => { … }) したときの push の後先(x -> y の順で push されたか y -> x の順で push されたか)を識別するうまい方法ってないんかな?

単純にタイムスタンプを一回付けてから引っぺがすだけ。

    public static class ReactiveEx
    {
        private static long _currentStamp;
        static long GetStamp()
        {
            return System.Threading.Interlocked.Increment(ref _currentStamp);
        }

        private class StampedItem<T>
        {
            public T Item { get; set; }
            public long Stamp { get; set; }
        }

        public static IObservable<TResult> CombineLatest<TLeft, TRight, TResult>(
            this IObservable<TLeft> leftSource,
            IObservable<TRight> rightSource,
            Func<TLeft, TRight, bool, TResult> selector )
        {
// ReSharper disable InvokeAsExtensionMethod
            return Observable.CombineLatest(
                leftSource.Select(leftItem => new StampedItem<TLeft> {Item = leftItem, Stamp = GetStamp()}), 
                rightSource.Select(rightItem => new StampedItem<TRight> {Item = rightItem, Stamp = GetStamp()}),
                (left,right) => selector( left.Item, right.Item, left.Stamp>right.Stamp ));
// ReSharper restore InvokeAsExtensionMethod
        }
    }

「Rxの神が出てくる前にネタは潰しておかなければならない。」等とは思っていない。

って書いたすぐに

神が出てきて

@kazuk @igeta Timestampというそのものなメソッドがあったりするので、細かい調整がしたい場合は、やっぱそれ使ってね、ということだと思います。と、さっきまで他に何か手はないかなー、と考えてたけど浮かびませんでした:)

Timestampを自作する必要なんて無いかもーって思って書き変えた。

    public static class ReactiveEx
    {
        public static IObservable<TResult> CombineLatest<TLeft, TRight, TResult>(
            this IObservable<TLeft> leftSource,
            IObservable<TRight> rightSource,
            Func<TLeft, TRight, bool, TResult> selector )
        {
// ReSharper disable InvokeAsExtensionMethod
            return Observable.CombineLatest(
                leftSource.Timestamp(), 
                rightSource.Timestamp(),
                (left,right) => selector( left.Value, right.Value, left.Timestamp<right.Timestamp ));
// ReSharper restore InvokeAsExtensionMethod
        }
    }

Timestamp がDateTimeOffsetってのは間隔次第ではきっとかぶるよねーって事で、本気にどっちが先かの厳密性を追う用途の場合には前バージョンを、そうでない場合には後者を使う事になると思います。

という訳で書いてみる事で新たな知識を得たと。

しかし、Rx のメソッド検索すると Windows Phone の方のリファレンスに飛ばされる…。</P

特定の型パラメータの選択肢でジェネリックを制約する


何度となく思ったジェネリックの制限

where T : byte | char

って事で byte か char しか受けないよって事ですな。

こういう場合、private な内部クラスでジェネリッククラスを作っておいて、選択肢はパラメータオーバーロードで提供します。

 

    public static class Extentions
    {
        private class ArrayClearer<T> where T : struct
        {
            public static void ClearArray(T[] arr)
            {
                for (int i = 0; i < arr.Length; i++)
                {
                    arr[i] = default(T);
                }
            }
        }
        public static void ClearArray( char[] array )
        {
            ArrayClearer<char>.ClearArray(array);
        }
        public static void ClearArray( byte[] array )
        {
            ArrayClearer<byte>.ClearArray(array);
        }
    }


    class Program
    {
        static void Main(string[] args)
        {
            Extentions.ClearArray(new byte[]{});
            Extentions.ClearArray(new char[]{});
            Extentions.ClearArray(args);
            Extentions.ArrayClearer.ClearArray(args);
        }
    }

手間ばっかりなんでよっぽどの事でない限りこんな事する必要はないと思いますけど。

この例の最後の Extentions.ClearArray(args) は当然に string[] を受けるのが無いのでエラーに、ArrayClearerに直接触ろうとするのはprivateだから駄目と断られます。

きっちり制約したいという条件ではこんな書き方もできますよっと。

 

#でも制約で蹴った物が後から必要になったりすると直すのは手間だからご注意を