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 鍵後,就大功告成了.

2011年6月23日 星期四

WCF Data Service 的優缺點

最近一直與 WCF Data Service 打交道。也漸漸了解這個技術的優勢與缺點了。整理如下

優點1:RESTful

如果使用WCF來開發一個 CustomerService,就免不了如下的Method

AddCustomer(Customer customer);
DeleteCustomer(int id);
UpdateCustomer(Customer customer);
FindCustomers(string condition);

新刪修查,至少要發佈4個 Method。如果使用 WCF Data Service,只需要一個 Url 來定位即可。如果再使用 Entity Framework,更是方便。

優點2:WCF Data Service Client Library

使用 WCF Data Service Client Library 來查詢 OData的資料,可再進一步使用 LINQ 的語法來查詢資料。新增/刪除/修改資料時,更像是在操作 Entity Framework 一般的方便。見更新資料服務 (WCF Data Services)

優點3:跨平台

這個年代,服務不能跨平台,就沒有人要使用。因此,WCF Data Service 是非常簡單的 Url 定址,與 xml 純文字的回傳資料,再加上 HTTP 的 Verbs,即可完成簡單的操作。跨平台,是最大的賣點。看一下 oData SDK,各式各樣的平台與技術都被支援哦!

缺點1:查詢大都只能支援 HTTP Method = GET

WCF Data Service要查詢資料目前大都只支援 GET。當有大量資料要上傳到伺服器時,由於 HTTP GET 有 Url的長度限制(  通常為 8096 bytes),所以只好轉用 POST。但 WCF Data Service Client Library 支援 POST 的能力很差哦!連呼叫都必須使用 HttpWebRequest 或 WebClient 來呼叫,相對來說實在很難用。

缺點2:支援LINQ 語法尚差

查詢資料服務 (WCF Data Services),初看之下看來很強悍,但 check LINQ 考量 (WCF Data Services) 中的「不支援的 LINQ 方法」一節,不支援的 LINQ 語法還真多。最常用的 Join,Count竟然也不支援,聲勢立即弱了下來。

缺點3:不適合大量資料上傳到伺服器

由於缺點1的關係,想要傳較多資料到伺服器時,就必須使用 WebInvoke 的 service operation。在Service Operation上,並不能使用類似 WCF Service 的 Stream 來讀取 POST 的資料。原因很簡單,因為 Stream 並無法自動發佈成 metadata。因此,如果要讀POST 的內容,必須使用 ASP.NET 的 HttpContext.Current.Request.Form 來讀取。因此程式碼就完全相依服 ASP.NET了。之後有空,就寫另一篇來介紹這個作法。

缺點4:只能跑 Http,不能跑 binary

RESTful 使用了 Url 來定址,新刪修查分別對應 POST, DELETE, PUT,GET等 HTTP Method(Verbs),所在只能跑 http 的 protocal,無法跑 binary 來傳資料。傳輸資料量變大後,效能就會比較差。幸好, IIS 有動態壓縮的功能,可以有效減少傳輸的資料量。

結論

看起來好像缺點比優點多一些,但是,跨平台卻是相當大的優勢。未來是 HTML5的世界,WCF Data Service 一定大有可為。

安裝 HTML5 Support for Visual Studio 2010

HTML5 Support for Visual Studio 2010

微軟的 Visual Studio 2010 終於支援 HTML5 的編輯了。看了Announcing the Web Standards Update - HTML5 Support for the Visual Studio 2010 Editor,就下載了 Visual Studio Web Standards Update。

問題

怪事發生了,一旦執行 Web Standards Update.msi,就會出現 Service pack 1 for visual studio 2010 must be installed 的警告訊息。我的確有安裝 SP1 for VS2010 啊!

SNAGHTML2ac48c

解決

如果有安裝 Windows SDK 的話,可從修改 msi 檔著手。以我的機器為例,在 C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\bin\ 下有個 Orca.msi,安裝後,執行 Orca。

image

打開 Web Standard Update.msi 後, 在左方 Tables 內選 CustomAction, 在 VSDCA_VsdLaunchConditions 上按右鍵 ,執行 Drop Row功能。

SNAGHTML3205d8

記得按儲存鍵。

image

執行

由於我們修改了 msi,數位簽章就失效了,執行時會出現如下圖的警告訊息,這是正常的。按「Run」以繼續執行。

SNAGHTML34ad88

然後,就可以安裝了。

SNAGHTML35d9c4

SNAGHTML364f42

執行 Visual Studio 2010.建立一個新的 Web Application後,可以選擇 HTML5 來檢查格式。

image

當我們打入 <vi時,會出現 video 的 intellisense (智能提示)。果然好用。

image

參考

我將修改好的 msi 放在這裡,有需要的人可以自行下載。

2011年6月22日 星期三

WPF的UI更新方式

緣由

在以往的 VB6,或者是 Windows Form 應用程式中,更新 UI的方式極為簡單,往往只是 Application.DoEvents 就可以更新。Windows Form 中更有 Invoke 與 BeginInvoke 可以彈性地使用。

那在 WPF 中,要如何更新 UI 的內容呢?

範例1:Bad Example

當然,要從一個不正確的範例開始。

Ex1Bad.xaml

<Window x:Class="WpfApplication10.Ex1Bad"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Ex1Bad" Height="300" Width="300">
  <StackPanel>
    <Label Name="lblMsg" Content="Nothing" />
    <Button Content="Start" Name="btnStart" Click="btnStart_Click"/>
  </StackPanel>
</Window>

Ex1Bad.xaml.cs

using System.Threading;
using System.Windows;

namespace WpfApplication10
{
  public partial class Ex1Bad : Window
  {
    public Ex1Bad()
    {
      InitializeComponent();
    }

    private void btnStart_Click(object sender, RoutedEventArgs e)
    {
      lblMsg.Content = "Starting...";
      Thread.Sleep(3000); //執行長時間工作
      lblMsg.Content = "Doing...";
      Thread.Sleep(3000); //執行長時間工作
      lblMsg.Content = "Finished...";
    }
  }
}

這裡以 Thread.Sleep(3000)讓 UI Thread 睡個3秒鐘,來模擬長時間的工作。

這是個常見的程式碼,但卻是沒有用。在 Windows Form 的 API 中有 Application.DoEvents() 可以呼叫。WPF 中沒有類似的嗎?

範例2:使用Windows Form的 DoEvents

原來,WPF 中雖然沒有類似的 api 可以呼叫,但仍可以直接呼叫 Windows Form 的 Application.DoEvents.當然,需要引用 System.Windows.Forms.dll。

Ex2WinformDoEvents.xaml

using System.Threading;
using System.Windows;
using swf = System.Windows.Forms;

namespace WpfApplication10
{
  public partial class Ex2WinformDoEvents : Window
  {
    public Ex2WinformDoEvents()
    {
      InitializeComponent();
    }

    private void btnStart_Click(object sender, RoutedEventArgs e)
    {
      lblMsg.Content = "Starting...";
      swf.Application.DoEvents();
      Thread.Sleep(3000); //執行長時間工作
      lblMsg.Content = "Doing...";
      swf.Application.DoEvents();
      Thread.Sleep(3000); //執行長時間工作
      lblMsg.Content = "Finished...";
    }
  }
}

在更新UI後,呼叫 swf.Application.DoEvents(),就可以更新 UI 了。這樣的方式與之前的 VB6是一模一樣的手法。

範例3:WPF DoEvents

哦?WPF 沒有 DoEvents 可以使用,只能呼叫老前輩Windows Form 的 API 嗎?也不是。在 DispacherFrame 文章中就有sample.

Ex3WPFDoEvents.xaml.cs

using System;
using System.Security.Permissions;
using System.Threading;
using System.Windows;
using System.Windows.Threading;

namespace WpfApplication10
{
  public partial class Ex3WPFDoEvents : Window
  {
    public Ex3WPFDoEvents()
    {
      InitializeComponent();
    }

    private void btnStart_Click(object sender, RoutedEventArgs e)
    {
      lblMsg.Content = "Starting...";
      DoEvents();
      Thread.Sleep(3000); //執行長時間工作
      lblMsg.Content = "Doing...";
      DoEvents();
      Thread.Sleep(3000); //執行長時間工作
      lblMsg.Content = "Finished...";
    }

    [SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)]
    public void DoEvents()
    {
      var frame = new DispatcherFrame();
      Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
          new DispatcherOperationCallback(ExitFrame), frame);
      Dispatcher.PushFrame(frame);
    }

    public object ExitFrame(object f)
    {
      ((DispatcherFrame)f).Continue = false;

      return null;
    }

    public static void DoEvents2()
    {
      Action action = delegate { };
      Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Input, action);
    }
  }
}

DoEvents() 與 DoEvents2() 的效果相同。

DoEvents is Evil

DoEvents 這麼好用,為什麼WPF還要發明 Dispatcher,或 Windows Form 的 BeginInvoke 這些 API  呢?

跑以下的程式碼看看吧。

private void btnEvil_Click(object sender, RoutedEventArgs e)
{
  for (int i = 0; i < 10000000; i++)
  {
    System.Windows.Forms.Application.DoEvents();
  }
  MessageBox.Show("Ok");
}

執行時,記得打開工作管理員看看CPU 的負載,會持續飆高一斷時間。雖然 UI 沒有任何的更新,為何 CPU 會飆高呢?

DoEvent 的原理是execution loop,也就是以迴圈的方式來查詢是否有要更新的訊息(message)。一看到迴圈,各位看倌就知道是怎麼回事了吧。

範例3中的WPF 的 DoEvents 也是相同的道理。

範例4:BackgroundWorker

有沒有較正常的方式來更新 UI 呢?看一下Ex1Bad.xaml.cs的設計方式吧。更新lblMessage後執行一段工作,這基本上是同步的寫作方式。在 UI Thread 上執行工作,本來就會使得 UI 停頓,使用者感到不方變。

正確的方式,是使用BackgroundWorker來執行長時間的工作,並以非同步的方式更新在 UI Tread 上的UI內容。

Ex4BackgroundWorker.xaml.cs

using System;
using System.ComponentModel;
using System.Threading;
using System.Windows;
using System.Windows.Controls;

namespace WpfApplication10
{
  public partial class Ex4BackgroundWorker : Window
  {
    public Ex4BackgroundWorker()
    {
      InitializeComponent();
    }

    private void btnStart_Click(object sender, RoutedEventArgs e)
    {
      ExecuteLongTimeWork(lblMsg, "Starting");
      ExecuteLongTimeWork(lblMsg, "Doing");
      ExecuteLongTimeWork(lblMsg, "Finished");
    }

    private void ExecuteLongTimeWork(Label label, string message)
    {
      var backgroundWorker = new BackgroundWorker();
      backgroundWorker.DoWork += (s, o) => {
        Thread.Sleep(3000); //執行長時間工作
      };

      backgroundWorker.RunWorkerCompleted += (s, args) =>
                                               {
                                                 Dispatcher.BeginInvoke(new Action(() =>
                                                 {
                                                   label.Content = message;
                                                 }));
                                               };
      backgroundWorker.RunWorkerAsync();
    }
  }
}

BackgroundWorker 工作方式,是建立一個新的 Thread 來執行 DoWork 的event handler,執行完畢後,再執行 RunWorkerCompleted 的event handler。因此,我們需要的是在RunWorkerCompleted 的event handler 中更新的 UI。

更新 UI 時,又使用 Dispatcher 來更新 UI。基本上,Dispatcher 是一個 Queue 的概念,凡是使用 Dispatcher 來更新 UI,WPF 會照 DispatcherPriority 的優先順序來更新 UI。

結論

雖然只是小小的UI 更新方式,也是有不少的學問呢!

Share with Facebook