2011年7月26日 星期二

While trying to generate your tests, the following errors occurred

當我使用 Visual Studio 2010 建立一個 Unit Test 時,發生了下面的錯誤

image

While trying to generate your tests, the following errors occurred:
Value cannot be null.
Parameter name: key

找了一下,原因很簡單。我的 TestProject 已經有相同名稱的檔名了,但在不同的目錄。VS 2010 發現有相同的 class (Full name),就會發生這樣的錯誤。

解決方法呢?就是改檔名或改 namespace 即可。

2011年7月7日 星期四

ASP.NET WebForm 與 WPF 在取得使用者輸入資料時的差異

緣由

這個標題下的有些古怪。

我的專案,原本都以 ASP.NET為主。排程的程式大都是 Console 的程式。然而最近接了一個 Client Application 的專案。

經過比較後,我選擇了較新的 WPF,而放棄了 WinForm。問題來了,專案中的成員會問到WPF中如何在DataGrid 取得使用者所選擇ComboBox 的SelectedIndex。

為了演示 WebForm 與 WPF 兩者的差異,我寫了這個 demo.

這個 Demo其實很簡單:設定10個 Emp 的性別。然而分別以 WebForm 與 WPF 做一次,再討論兩者的設計方式。

資料來源

雖然是不同的應用程式,但有相同的資料,故,將兩者相同的程式放在 CommonLib 中.

using System.Collections.Generic;
using System.Linq;

namespace CommonLib
{
  public class Emp
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public int Gender { get; set; }
  }
  public class GenderStruct
  {
    public int Id { get; set; }
    public string Name { get; set; }
  }
  public class MyDataSource
  {
    public List<Emp> GetAllEmps()
    {
      var q = from i in Enumerable.Range(1, 10)
              select new Emp()
              {
                Id = i,
                Name = string.Format("N{0}", i),
                Gender = 1
              };
      return q.ToList();
    }

    public List<GenderStruct> GetAllGenders()
    {
      return new List<GenderStruct>
                      {
                        new GenderStruct { Id = 1, Name = "Boy"},
                        new GenderStruct { Id = 2, Name = "Girl"},
                        new GenderStruct { Id = 3, Name = "Gay"},
                      };
    }
  }
 
}

WebForm

ASP.NET WebForm的設計其實真的很麻煩,但用習慣了,也就習以為常。下是在 aspx 中放入一個 GridView 以顯示員工的資料。使用者輸入性別再按下 button,網頁顯示所有員工的最後值。

Default.aspx

<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" 
  DataKeyNames="Id" onrowdatabound="GridView1_RowDataBound">
  <Columns>
    <asp:BoundField DataField="Id" HeaderText="Id" />
    <asp:BoundField DataField="Name" HeaderText="Name" />
    <asp:TemplateField HeaderText="Gender">
      <ItemTemplate>
        <asp:DropDownList runat="server" ID="ddl">
        </asp:DropDownList>
      </ItemTemplate>
    </asp:TemplateField>
  </Columns>
</asp:GridView>
<asp:Button ID="Button1" runat="server" onclick="Button1_Click" Text="Button" />

Default.aspx.cs

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

namespace WebApplication1
{
  public partial class _Default : System.Web.UI.Page
  {
    protected void Page_Load(object sender, EventArgs e)
    {
      if (!Page.IsPostBack)
      {
        var dataSource = new MyDataSource();
        var emps = dataSource.GetAllEmps();
        GridView1.DataSource = emps;
        GridView1.DataBind();
      }
    }

    protected void GridView1_RowDataBound(object sender, GridViewRowEventArgs e)
    {
      var dataSource = new MyDataSource();
      var allGender = dataSource.GetAllGenders();
      if (e.Row.RowType == DataControlRowType.DataRow)
      {
        var ddl = e.Row.FindControl("ddl") as DropDownList;
        ddl.DataSource = allGender;
        ddl.DataValueField = "Id";
        ddl.DataTextField = "Name";
        ddl.DataBind();
      }
    }

    protected void Button1_Click(object sender, EventArgs e)
    {
      //由 GridView1 讀取資料
      foreach (GridViewRow row in GridView1.Rows)
      {
        var ddl = row.FindControl("ddl") as DropDownList;
        var gender = ddl.SelectedIndex;
        var id = int.Parse(row.Cells[0].Text);
        string message = string.Format("Id:{0}, Gender:{1}", id, gender);
        Response.Write(message + "<br />");
      }
    }
  }
}

這一段程式寫的還真不怎麼樣,竟然還用 Response.Write 這種古老 ASP 的寫作方式?重點不是在演示如何取得資料,而是 ASP.NET 先天上的限制: Stateless

由於 Web 的 Stateless 的特性,使得Server 端不會記得 Client 曾經輸入或給過的值。因此,當網頁第一次進入時取得所有 Emp 的資料,並在 GridView 的 RowDataBound 事件發生時,為每個性別欄位設定 DropDownList。

之後,使用者輸入完畢後,Submit 回到 Server後,emp 及 gender 的資料都已經不見了,只剩下 GridView 這個 Control 中的值能幫我們留下使用者最終的選擇。

在不得已的情況下,我們必須研究 GridView 的組成,如 GridView 的 Row,Row 下有 Cells,再使用 FindControl 取得 DropDownList…等等。image

WPF

在 WPF 中,由於是 Stateful,相同的功能,寫作方式相對 ASP.NET 大為進步。

MainForm.xaml

<DataGrid AutoGenerateColumns="False" Height="206" HorizontalAlignment="Left" Margin="12,12,0,0" Name="dataGrid1" VerticalAlignment="Top" Width="288" >
  <DataGrid.Columns>
    <DataGridTextColumn Header="Id" Binding="{Binding Path=Id}" />
    <DataGridTextColumn Header="Name" Binding="{Binding Path=Name}" />
    <DataGridTextColumn Header="Gender" Binding="{Binding Path=Gender}" />
    <DataGridComboBoxColumn Header="Gender2" x:Name="cbGender" SelectedValuePath="Id" DisplayMemberPath="Name" SelectedValueBinding="{Binding Gender}"   />
  </DataGrid.Columns>
</DataGrid>
<Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="359,115,0,0" Name="button1" VerticalAlignment="Top" Width="75" Click="button1_Click" />

MainForm.xaml.cs

public partial class MainWindow : Window
{
private List<Emp> _emps;
private List<GenderStruct> _allGenders;
public MainWindow()
{
  InitializeComponent();
}

private void Window_Loaded(object sender, RoutedEventArgs e)
{
  //取得資料並放到 Window 的變數中。因為Stateful,所以可以這樣做
  var dataSource = new MyDataSource();
  _emps = dataSource.GetAllEmps();
  _allGenders = dataSource.GetAllGenders();

  dataGrid1.ItemsSource = _emps;
  cbGender.ItemsSource = _allGenders;
}

private void button1_Click(object sender, RoutedEventArgs e)
{
  var emp = dataGrid1.SelectedItem as Emp;
  var message = string.Format("Name: {0}, Gender: {1}", emp.Name, emp.Gender);
  MessageBox.Show(message);
}
}

值得注意的是,在 Window Loaded 後,取得所有 Emp 及 Gender 資料後,直接放到 Window 的 private member 中,讓後續的資料繫結(databind) 作業直接存取window private member即可。

使用者輸入完畢後,更新後的資料「直接」反應到資料繫結的對象(即 _emps, _allGenders),不必像 WebForm一樣透過 Control 間接取得資料。

image

結論

這個範例中,主要演示了 ASP.NET WebForm 與 WPF 的先天上的差異(Stateless vs Stateful),並且由於 WPF 雙向繫結的功能,使用者的輸入可以直接更新到資料來源,遠較WebForm 來的直覺與方便。

2011年7月5日 星期二

Web deployment task failed. Unrecognized attribute 'serviceAutoStartMode'

經由上次介紹的 設定 IIS 為自動啟動 (Auto-Start)後,原先在 TFS 的 Web 自動部署 反而失敗了。訊息如下:

C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v10.0\Web\Microsoft.Web.Publishing.targets (3588): Web deployment task failed.((2011/7/5 上午 08:16:45) An error occurred when the request was processed on the remote computer.)  (2011/7/5 上午 08:16:45) An error occurred when the request was processed on the remote computer. Filename: \\?\C:\Windows\system32\inetsrv\config\applicationHost.config Line number: 176 Error: Unrecognized attribute 'serviceAutoStartMode'

看來,目前 TFS 的 Publishing 尚不知道  AppFabric 的serviceAutoStartMode 屬性。只好在測試機上不使用 IIS Auto Start 的功能。

2011年7月4日 星期一

設定 IIS 為自動啟動 (Auto-Start)

第一次總是比較慢

以前在dotnet 1.0 時介紹 ASP.NET 的工作原理時,總是滿懷興奮向客戶提到compilation 的好處,就是程式執行後,會留下編譯(compilation) 的結果,下次相同程式執行時,不必再編譯了. 但此處有個缺點,第一個 Request,總是最倒楣的,不但要等 compilation,而且要等 IIS 建立執行序。

在 dotnet 2.0 後,有了 precompilation的機制。作法是部署程式,以手動執行 aspnet_compiler 來扮演那第一個倒楣的Reqeust。

至於等 IIS 建立執行序這回事呢,還是無解呢。

Auto-Start

在 IIS 7.5 後(Windows Server 2008 R2 or Windows 7),此問題終於有解了。作法是修改 C:\Windows\System32\inetsrv\config\applicationHost.config,並且設定兩個地方。

1  修改應用程式集區 (Application Pool) 為自動啟動:搜尋 <applicationPools,並將對應的 application pool 加上 startMode=”AlwaysRunning”

<applicationPools>
    <add name="ASP.NET v4.0" autoStart="true" managedRuntimeVersion="v4.0" startMode="AlwaysRunning">
        <processModel identityType="ApplicationPoolIdentity" loadUserProfile="true" />
    </add>
  
2  修改應用程式為自動啟動:搜尋 <sites,並將對應的應用程式加上 serviceAutoStartEnabled="true"的屬性值

 <sites>
    <site name="Default Web Site" id="1" serverAutoStart="true">
        <application path="/">
            <virtualDirectory path="/" physicalPath="c:\inetpub\wwwroot" />
        </application>
        <application path="/MyApp" applicationPool="ASP.NET v4.0" serviceAutoStartEnabled="true" serviceAutoStartProvider="Service" serviceAutoStartMode="All">
            <virtualDirectory path="/" physicalPath="E:\Projects\Smart\Dev\Rsi.Smart2G\Rsi.Smart2G.WebServices" />
        </application>

Windows Server AppFabric

使用NotePad 修改 config總是怕怕的,尤其是 IIS 此等重要的設定,怎麼可以用如此簡易的工具來設定呢?客戶的資訊人員萬一修改錯誤,又得呼叫我們苦命的Vender 工程師,誰會給我們加班費呢?難道微軟沒有聽到我們的心聲嗎?

有的,這個 UI 設定工具,就是之前介紹的 Windows Server AppFabric。安裝後,只要簡單地在應用程式上按右鍵/Manage WCF and WF Services/Configure…,

SNAGHTML7d76c6

接下來在左方功能表中選到 Auto Start,再選擇 Enable(all services will auto start)的選項。

image

按 Apply 鍵後,就大功告成了.

Share with Facebook