2010年8月21日土曜日

ListBoxのアイテムとして表示するUserControlの幅をひろげる

XAMLで期待したとおりにUIを表示するうちでいくつか大変な箇所があるが、今回紹介するのもそのうちの一つだ。

ケース1
ListBox内部にUserControlをアイテムとして表示する際に、内部に表示したUserControlが下図のようにListBoxの幅いっぱいまで広がってくれないことがある。


実際には下図のように表示したい。

ここからコードを交えて解説してく。

SampleUserControl - ListBox内部に表示するUserControl
SampleUserControl.xaml
<Border BorderBrush="RoyalBlue" BorderThickness="2" CornerRadius="3" HorizontalAlignment="Stretch">
  <TextBlock x:Name="_text" TextWrapping="Wrap" />
</Border>

SampleUserControl.xaml.cs
public SampleUserControl()
{
    InitializeComponent();
    _text.SetBinding(TextBlock.TextProperty, new Binding() { Source = this, Path = new PropertyPath("Text") });
}

public string Text
{
    get { return (string)GetValue(TextProperty); }
    set { SetValue(TextProperty, value); }
}
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(SampleUserControl), new PropertyMetadata("Sample Text"));

上記のコードはListBox内部に表示するUserControlの詳細になる。ListBoxからテキストをDataBindingできるようにTextをDependency Propertyにしている以外はいたって普通のコードだ。


MainPage.xaml
<UserControl.Resources>
        <Style x:Key="ListBoxItemContainerStyle" TargetType="ListBoxItem">
            <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
        </Style>
    </UserControl.Resources>
    <Border BorderBrush="Tomato" CornerRadius="2" Padding="2" Margin="2" BorderThickness="2" Width="300" >
        <ListBox ItemsSource="{Binding TextList}"
                     ItemContainerStyle="{StaticResource ListBoxItemContainerStyle}"
                     Margin="5,5" >
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <application:SampleUserControl Text="{Binding}" />
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Border>

MainPage.xaml.cs
public MainPage()
{
 InitializeComponent();
 TextList = new List<string> { 
  "Silverlightサンプル",
  "マツオソフトウェア",
  "Yoo Matsuo",
 };        
 this.DataContext = this;
}

 public List<string> TextList { get; set; }

MainPage.xamlのポイントはListBoxのItemContainerStyleに渡しているResourcesで定義したStyleだ。このStyleでHorizontalContentAlignmentをStrechとしている。これでListBox内部のアイテムがListBoxの幅いっぱいまで広がって表示されるようになる。

ケース2
ListBox内部にUserControlをアイテムとして表示する際に、内部に表示したUserControlが下図のようにListBoxの幅を超えてどこまでも広がっていってしまうことがある。


ListBoxの幅で折り返したい。

この対処方はしごく簡単で、ListBoxの宣言部に下記を追加すればよい。
ScrollViewer.HorizontalScrollBarVisibility="Disabled"

これでListBoxの幅で折り返されるはずだ。ちなみにSampleUserControl.xamlでTextBlockのTextWrapping="Wrap"を設定しているが、これを忘れると今回のサンプルではUserControlの表示そのものがListBoxの幅で切り取られてしまうので注意して欲しい。

Silverlight起動時にError Code 2103で例外が発生したら

今まで動いていたSilverlightが突然起動しなくなるときがある。ブラウザが出力するエラーは「初期化エラー 2103」などというばかりでよく分からない。思い当たる節は名前空間をプロジェクト全体でいじったことぐらい・・・、とくるなら下記の方法を実践してもらいたい。

プロジェクトのプロパティ→Silverlightタブ
Startup Objectのドロップダウンリストを上下させて新しい名前空間のAppファイルを参照するようにする。



名前空間を一括置換などで変更している場合はここが変更されていないことがままある。その場合はSilverlightが起動に必要な情報を収集できなくなり、前述のエラーが出力されるようだ。

ExcelのAutomationのTipsあれこれ

業務でExcelで来たデータをSQL Serevrに流し込む必要があり、ExcelのAutomation用のツールを作ったのでそのときのTipsをいくつか紹介。

Excel Automationの概要自体は下記のリンクを参考にして欲しい。
How to automate Microsoft Excel from Microsoft Visual C#.NET


必ずCOMインスタンスを開放する
Excel Automationでもっともつまづきやすいのが、アプリが終了してもいつまでもExcelのプロセスが居座ることだ。これは色々と理由があるが下記のコードのような状況が最も陥りやすい。

var application = new Excel.Application();
var workBook = application.Workbooks.Open("path", Type.Missing.... );
var sheet = workBook.Sheets[2];

Marshal.FinalReleaseComObject(sheet);
workBook.Close();
Marshal.FinalReleaseComObject(workBook);
application.Quit();
Marshal.FinalReleaseComObject(application);

問題はOpenを呼んでいるところだ。実はapplication.Workbooks.Openを呼び出した際に、内部的にapplication.WorkbooksのCOMインスタンスが生成されている。そのためCOMインスタンスを開放するためにWorkBooksもMarshal.FinalReleaseComObjectしなければならないのだが、ここではWokrBooksをハンドルしていないためにそれができない。正しくは下記のようになる。

var application = new Excel.Application();
var workBooks = application.Workbooks;
var workBook = workBooks.Open("path", Type.Missing.... );
var sheet = workBook.Sheets[2];

Marshal.FinalReleaseComObject(sheet);
workBook.Close();
Marshal.FinalReleaseComObject(workBook);
Marshal.FinalReleaseComObject(workBooks);
application.Quit();
Marshal.FinalReleaseComObject(application);


これでExcelのプロセスが居座ることは解決できたが、例外処理が起きたときや、Cellから値を取得するためにCOMインスタンスを生成したときなど、開放処理を常に行うように気を使うのはなかなか骨が折れる。そのため、開放処理を簡便に行えるようなWrapperを作ることにした。

ちなみにこのアイディアは下記のStackoverflowのGary McGillから拝借している。
c# and excel automation - ending the running instance


Wrapperクラス
class ComWrapper<T> : IDisposable
{
    public ComWrapper(T t)
    {
        ComObject = t;
    }
    public void Dispose()
    {
        Marshal.FinalReleaseComObject(ComObject);
    }
    public T ComObject { get; set; }
}

使っているところ
using (var application = new ComWrapper(new Application()))
{
 try
 {
    using (var workbooks = new ComWrapper(application.ComObject.Workbooks))
    {
        using (var workbook = new ComWrapper(workbooks.ComObject.Open(filePath, ...)))
        {
            try
            {
                using (var worksheet = new ComWrapper(workbook.ComObject.Sheets[1]))
                {
                    // なにか処理
                }
            }
            finally
            {
                workbook.ComObject.Close();
            }
        }
    }
 }
 finally
 {
    application.ComObject.Quit();
 }
}

おまけ - Cellの値を取得
for( var rowIndex = 1; rowIndex < 10; rowIndex ++)
{
 var colIndex = 0;
 using (var cell1 = new ComWrapper(worksheet.ComObject.Cells[rowIndex, ++colIndex]))
 using (var cell2 = new ComWrapper(worksheet.ComObject.Cells[rowIndex, ++colIndex]))
 {
 // 処理
 var value = cell1.ComObject.Value
 }
}


usingを使うことによりスコープ範囲外到達時に常にDisposeが呼ばれ、そのDispose内部でCOMインスタンスの開放を行っているため、COMの開放というわずらわしい処理を気にする必要がまったくなくなった。これでロジック部分に集中できるだろう。