實作非同步模式的 TCP 用戶端

2 comments
TcpClient 類別是以 Socket 類別為基礎所建立的抽象層 TCP 服務,它提供簡單的方法以連接、傳送和接收網路間的資料,用來簡化 TCP 用戶端應用程式的開發。TcpClient 提供同步與非同步兩種通訊模式,本文將會著重探討如何使用非同步程式撰寫模型來處理網路服務要求。

同步模式
在一般的情況下我們都使用同步的封鎖模式來建立 Socket 應用程式,因為這種方式最簡單也最直接。以下範例將會以同步作業的方式發送 HTTP 請求:
using (TcpClient client = new TcpClient("www.msn.com", 80))
{
using (NetworkStream stream = client.GetStream())
{
byte[] send = Encoding.UTF8.GetBytes("GET HTTP/1.0 \r\n\r\n");
stream.Write(send, 0, send.Length);
byte[] bytes = new byte[client.ReceiveBufferSize];
int bytesRead = stream.Read(bytes, 0, (int)client.ReceiveBufferSize);
String data = Encoding.UTF8.GetString(bytes);
char[] unused = { (char)data[bytesRead] };
Console.WriteLine(data.TrimEnd(unused));
}
}

在上例中,我們使用 TcpClient 的建構式進行同步的連接嘗試,並透過 Stream 通訊端同步收發網路資料。在這段時間內,執行同步要求的執行緒會因為等候網路作業完成而無法再執行其他工作。如果該執行緒是 UI 執行緒,那麼應用程式就被封鎖並停止回應使用者的輸入。雖然使用同步封鎖的 Socket 從觀念上來說比較容易使用,然而如果你的應用程式需要保持快速回應、提升延展性及可靠性,那麼就不應該使用同步封鎖的作業方式。

非同步模式
非同步用戶端通訊端使用標準 .NET Framework 非同步程式撰寫模型,讓你的應用程式不會因為等候同步作業完成的執行緒而被封鎖。因為非同步作業(Asynchronous Operation)會在不同的執行緒上(在背景中)執行,所以應用程式可以在呼叫非同步方法(BeginOperationName)的執行緒上繼續執行指令。例如,在 Windows 應用程式中,非同步執行作業將會委派給背景執行緒,而使用者介面執行緒(在前景中)在作業執行時仍可以保持回應狀態。


接下來,我們將以 Windows Form 應用程式來逐步建構非同步傳輸的 HTTP 用戶端程式。首先,必須先建立用來在非同步呼叫間傳送狀態資訊的狀態物件。然後,呼叫 TcpClient 物件的 BeginConnect 方法開始遠端主機連接的非同步要求。
TcpClient client = new TcpClient();
StateObject state = new StateObject(client);
state.Data.AppendFormat("GET {0} HTTP/1.0\r\n", url);
state.Data.AppendFormat("Host:{0}\r\n\r\n", hostName);

client.BeginConnect(hostName, 0,
new AsyncCallback(EndConnectCallback), state);

當完成非同步連線要求時,系統會回呼實作 AsyncCallback 委派的 EndConnectCallback 方法。在這個方法中,我們必須呼叫 EndConnect 方法來結束擱置的非同步連接要求。下列程式碼實作了 EndConnectCallback 方法:
private void EndConnectCallback(IAsyncResult ar)
{
StateObject state = (StateObject)ar.AsyncState;
TcpClient client = state.Client;

try
{
client.EndConnect(ar);

if (client.Connected)
{
NetworkStream stream = client.GetStream();
if (stream.CanWrite)
{
byte[] send = Encoding.UTF8.GetBytes(state.Data.ToString());
stream.BeginWrite(send, 0, send.Length,
new AsyncCallback(EndWriteCallback), state);
}
}
else
{
DisplayStatus(string.Format("Ready (last error: {0})", "Connect Failed!"));
}
}
catch (Exception ex)
{
client.Close();
DisplayStatus(string.Format("Ready (last error: {0})", ex.Message));
}
}

在用戶端通訊端讀寫資料時,我們需要定義一個能儲存非同步作業相關資訊的狀態物件。
internal class StateObject
{
public readonly TcpClient Client;
public readonly byte[] Buffer;
public readonly StringBuilder Data;
public readonly int BufferSize = 1024;

public StateObject(TcpClient client)
{
this.Client = client;
this.Data = new StringBuilder();
this.Buffer = new byte[this.BufferSize];
}
}

當成功連接到遠端主機後,接著會透過 Stream 通訊端的 BeginWrite 方法以非同步方式將 HTTP 請求字串寫入網路資料流,並於作業完成後回呼實作 AsyncCallback 委派的 EndWriteCallback 方法,這個方法會在網路資料流做好接收準備時接收遠端主機回應的資料。
private void EndWriteCallback(IAsyncResult ar)
{
StateObject state = (StateObject)ar.AsyncState;
TcpClient client = state.Client;

try
{
NetworkStream stream = client.GetStream();
stream.EndWrite(ar);

state.Data.Length = 0;
if (stream.CanRead)
{
stream.BeginRead(state.Buffer, 0, state.BufferSize,
new AsyncCallback(EndReadCallback), state);
}
}
catch (Exception ex)
{
client.Close();
DisplayStatus(string.Format("Ready (last error: {0})", ex.Message));
}
}

接著會呼叫 Stream 通訊端的 BeginRead 方法,以非同步方式從用戶端通訊端讀取資料,並於作業完成後回呼實作 AsyncCallback 委派的 EndReadCallback 方法,將遠端主機回應的資料讀入資料緩衝區並建立訊息字串。然後再次呼叫 BeginRead 方法,直到用戶端接收資料完畢。
private void EndReadCallback(IAsyncResult ar)
{
StateObject state = (StateObject)ar.AsyncState;
TcpClient client = state.Client;
NetworkStream stream = client.GetStream();

int bytesRead = stream.EndRead(ar);

if (bytesRead > 0)
{
state.Data.Append(Encoding.UTF8.GetString(state.Buffer, 0, bytesRead));
stream.BeginRead(state.Buffer, 0, state.BufferSize,
new AsyncCallback(EndReadCallback), state);
}
else
{
client.Close();
DisplayResults(state.Data.ToString());
}
}

最後,呼叫 DisplayResults 方法將非同步讀取作業所建立訊息字串顯示在 TextBox 控制項。
public void DisplayResults(string text)
{
if (InvokeRequired)
{
BeginInvoke(new Action<string>(DisplayResults), text);
return;
}

int hdrLen = 0;
if (-1 != (hdrLen = text.IndexOf("\r\n\r\n")))
{
uxHeader.Text = text.Substring(0, hdrLen);
char[] unused = { '\r', '\n', '\r', '\n' };
uxContent.Text = text.Substring(hdrLen).TrimStart(unused);
}
else
{
uxHeader.Text = string.Empty;
uxContent.Text = text;
}
}

因為 Windows Form 控制項的存取並沒有保證執行緒安全(Thread-safe),也就是說在當一個以上的執行緒同時存取共用資源時,就會發生競爭情形而導致狀態不一致或是死結的情況。所以,在多執行緒的表單中必須以安全執行緒的方式來存取控制項。因此,在 DisplayResults 方法中使用表單的 InvokeRequired 屬性來判斷呼叫端是否來自於建立控制項之執行緒以外的執行緒。如果是跨執行緒呼叫(Cross-Thread),則必須使用 Invoke 或是 BeginInvoke 方法來封送處理(Marshaling)至控制項的執行緒。當你在 Visual Studio 偵錯工具中執行應用程式,若有不安全的執行緒嘗試存取控制項時,偵錯工具會引發 InvalidOperationException 例外狀況:「跨執行緒作業無效: 存取控制項 control name 時所使用的執行緒與建立控制項的執行緒不同」。這也意味只有在偵錯期間才有可能發現這個例外狀況,如果應用程式完成建置與佈署,那麼即使有發生不安全的存取情況也不會引發錯誤,建議你在發現問題時加以修正。



Download sample code

參考資料:
Using an Asynchronous Client Socket
Asynchronous Client Socket Example
Asynchronous operations, pinning
以非同步的方式呼叫同步方法
使用非同步用戶端通訊端

繼續閱讀...

iTextSharp 中文字型解決方案

0 comments
iTextSharp 是由 iText(for Java)移植到 .NET 平台的開放原始碼專案。它是一個完全使用 C# 語言所撰寫的 PDF APIs,可以讓你即時建立 PDF 文件。然而,當你要使用 iTextSharp 來建立包含中文字型的文件時,就會面臨無法正常顯示中文的問題。這是因為 iTextSharp 只支援 14 個標準的 Type 1 英文字型,並沒有直接支援中文字型的顯示,取而代之的是你必須自行指定中文字型來源才能獲得解決,所以在此整理顯示中文字的幾種解決方案。

Windows 內建的中文字型
中文版 Windows 內建的中文字型檔可分為 True Type、True Type Collection 及 Open Type 三種字型格式。你可以在 %WINDIR%\Fonts 目錄下找到以 .ttf、.ttc 為延伸檔名的中文字型檔。以下範例將會使用標楷體字型來顯示文字:
Document document = new Document();
PdfWriter.GetInstance(
document,
new FileStream(@"cjk.pdf", FileMode.Create)
);
document.Open();

string fontPath = Environment.GetFolderPath(Environment.SpecialFolder.System) +
@"\..\Fonts\kaiu.ttf";
BaseFont bfChinese = BaseFont.CreateFont(
fontPath,
BaseFont.IDENTITY_H, //橫式中文
BaseFont.NOT_EMBEDDED
);
Font fontChinese = new Font(bfChinese, 16f, Font.NORMAL);
document.Add(new Paragraph(
"難得糊塗",
fontChinese
));
document.Close();

在上例中,你也可以使用 FontFactory 代替 BaseFont 來取得中文字型:
FontFactory.Register(fontPath);
Font fontChinese = FontFactory.GetFont("標楷體", BaseFont.IDENTITY_H, 16f);
document.Add(new Paragraph(
"吃虧是福",
fontChinese
));

CID 字型(Character Identity-keyed Fonts)
CID 字型格式通常被應用到中日韓(Chinese Japanese Korean, CJK)字元集。如果要使用 CJK 字型,你就必須搭配額外的亞洲語言包 iTextAsian-1.0.dll
Document document = new Document();
BaseFont.AddToResourceSearch("iTextAsian.dll");

PdfWriter.GetInstance(
document,
new FileStream(@"cjk.pdf", FileMode.Create)
);
document.Open();

BaseFont bfChinese = BaseFont.CreateFont(
"MHei-Medium",
"UniCNS-UCS2-H", // 橫式中文
BaseFont.NOT_EMBEDDED
);
Font fontChinese = new Font(bfChinese, 8);
document.Add(new Paragraph(
@"聰明難,糊塗難,
由聰明而轉入糊塗更難,
放一著,退一步,當下心安,非圖後來福報也。",
fontChinese
));
document.Close();

Download sample code

參考資料:
iText Tutorial: Getting fonts

繼續閱讀...

Printing PDF Documents using C#

0 comments
在報表應用中,PDF 是常被使用的文件格式,透過 iTextSharp 可以讓你即時(On-The-Fly)建立 PDF 文件。然而,如果你想自動將建立的文件內容輸出至印表機時,可能就會面臨苦無對策的窘境。因為,iTextSharp 並未提供實際的列印文件功能。所幸,這個問題可以藉由動態資料交換(Dynamic Data Exchange, DDE)技術獲得解決,也就是透過 DDE 從 Adobe Reader 來要求列印指定的文件檔案。在 .NET 中,我們可以使用 NDde 這個開放源碼專案,來輕鬆撰寫 DDE 連結的功能。

在接下來的程式碼範例中,除了參考「Printing PDF documents in C#」的做法外,同時亦採用 Clifton Nock 所著「Data Access Patterns」一書中的 Retryer Pattern。以下程式碼是實作 IRetryable 介面的類別,用來執行列印 PDF 文件的工作:
using System;
using System.Diagnostics;
using NDde;
using NDde.Client;

namespace PdfPrintExample
{
class PdfPrintOperation : IRetryable
{
private DdeClient _client;
private string _filePath;

public PdfPrintOperation(string filePath)
{
_filePath = filePath;
_client = new DdeClient("acroview", "control");
}

public bool Attemp()
{
try
{
_client.Connect();
return true;
}
catch (DdeException)
{
// ignore exception
}

return false;
}

public void Recover()
{
// try running Adobe Reader
Process p = new Process();
p.StartInfo.FileName = "AcroRd32.exe";
p.Start();
p.WaitForInputIdle();
}

public void Print()
{
_client.Execute("[DocOpen(\"" + _filePath + "\")]", 60000);
_client.Execute("[FilePrintSilent(\"" + _filePath + "\")]", 60000);
_client.Execute("[DocClose(\"" + _filePath + "\")]", 60000);
_client.Execute("[AppExit]", 60000);
}
}
}

以下是簡單的使用範例:
namespace PdfPrintExample
{
class Program
{
static void Main(string[] args)
{
PdfPrintOperation operation = new PdfPrintOperation(@"C:\sample.pdf");
Retryer retryer = new Retryer(operation);
retryer.Perform(10, 5000);
// print the file
operation.Print();
}
}
}

使用 Retryer Pattern 的好處在於連結 DDE Server 發生 DDEException 例外狀況時,可以自動嘗試重新連線,直到連接成功或是達到最大重試次數為止。

Download source code

參考資料:
Printing PDF documents in C#

繼續閱讀...

Useful User-Defined Functions for SQL Server

0 comments
在資料庫設計中,我們常會應用預儲程序(Stored Procedure)來對資料進行 CRUD 操作。除此之外,你也可以撰寫自訂函數來應付經常性的資料存取作業或是複雜的計算等等。在這裡,我羅列幾個我在過去開發商務系統中常會應用到的自訂函數。

fn_GetNextBusinessDay
指定起始日期,並傳回最近一個營業日期。
CREATE FUNCTION dbo.fn_GetNextBusinessDay(@StartingDate datetime)
RETURNS datetime
AS
BEGIN
DECLARE @NextBusinessDay datetime

IF DATEPART(dw, @StartingDate) = (7 - @@DATEFIRST + FLOOR(@@DATEFIRST / 7) * 7) --Saturday
SET @NextBusinessDay = DATEADD(d, 2, @StartingDate)
ELSE IF DATEPART(dw, @StartingDate) = (8 - @@DATEFIRST) --Sunday
SET @NextBusinessDay = DATEADD(d, 1, @StartingDate)
ELSE
SET @NextBusinessDay = @StartingDate

RETURN @NextBusinessDay
END

fn_GetTax
指定銷售額及稅率,並傳回應付的營業稅額。
CREATE FUNCTION dbo.fn_GetTax(@Amount int, @TaxRate real)
RETURNS real
AS
BEGIN
DECLARE @UntaxedPrice int, @Tax int
SET @UntaxedPrice = ROUND(CAST(@Amount AS real) / (1 + @TaxRate), 0)
SET @Tax = @Amount - @UntaxedPrice

RETURN @Tax
END

fn_FormatPercent
指定浮點數資料,並傳回百分比的表示式。
CREATE FUNCTION dbo.fn_FormatPercent(@InputNumber float)
RETURNS varchar(20)
AS
BEGIN
RETURN CAST(CAST(@InputNumber * 100 AS numeric(10, 0)) AS varchar(20)) + '%'
END

fn_PadLeft
將輸入字串靠右對齊,以特定的字元在左側填補至指定的總長度。
CREATE FUNCTION dbo.fn_PadLeft(@InputString varchar(1024), @PaddingChar char(1), @FieldLength int)
RETURNS varchar(1024)
AS
BEGIN
DECLARE @PaddingString varchar(1024)
IF @FieldLength > 0
SET @PaddingString = RIGHT(REPLICATE(@PaddingChar, @FieldLength) + @InputString, @FieldLength)
ELSE
SET @PaddingString = @InputString

RETURN @PaddingString
END

繼續閱讀...

Read Fixed-Width Text Records using C#

0 comments
雖然使用純文字檔格式(Flat File)來交換資料並不是很好的方式,然而在 XML 盛行的今日它仍是普遍存在的資料交換格式。純文字檔格式的資料列通常以特定的符號區隔,或是固定寬度欄位來區隔不同資料欄位。從程式員的觀點來看,處理固定寬度欄位格式的資料並不如使用特定的符號區隔的格式來得輕鬆。如果是使用傳統的方式從「位置」擷取資料,那將會更加麻煩。所以,本文在這裡就要介紹幾種處理固定寬度格式資料的解決方案。

以下範例文字檔內容是以固定寬度格式所描述的銷售訂單內容:
0001S0000120081101       2     5000
0001S0000220081101 10 2500
0001S0000320081101 1 10500

其檔案格式內容定義如下:
欄位名稱欄位長度
銷售店代號X(4)
訂單編號X(6)
訂單日期X(8)
銷售量9(8)
銷售金額9(9)

LINQ to Text Files
首先必須先定義一個延伸方法(Extension Method)來逐行讀取資料流,並建立可以供 LINQ 查詢的可列舉集合。在此我引用了 Eric White 在 LINQ to Text Files 中所提供的 StreamReaderSequence 類別:
public static class StreamReaderSequence
{
public static IEnumerable<string> Lines(this StreamReader source)
{
String line;

if (source == null)
throw new ArgumentNullException("source");
while ((line = source.ReadLine()) != null)
{
yield return line;
}
}
}

接下來使用規則運算式(Regular expression)依序定義群組編號及資料欄位長度:
Regex re = new Regex("^(?<1>.{4})" +
"(?<2>.{6})" +
"(?<3>.{8})" +
"(?<4>.{8})" +
"(?<5>.{9})",
RegexOptions.Compiled);

接著再搭配 LINQ 語法的 let 子句取得規則運算式比對所擷取的群組集合,然後併入查詢結果中:
using (StreamReader sr = new StreamReader(@"d:\orders.txt"))
{
var orders =
from line in sr.Lines()
let fields = re.Match(line).Groups
select new
{
StoreID = fields[1].Value.Trim(),
OrderNumber = fields[2].Value.Trim(),
OrderDate = DateTime.ParseExact(fields[3].Value.Trim(),
"yyyyMMdd", CultureInfo.InvariantCulture),
Quantity = int.Parse(fields[4].Value),
Amount = int.Parse(fields[5].Value)
};

foreach (var order in orders)
{
Console.WriteLine("{0},{1},{2},{3},{4}",
order.StoreID,
order.OrderNumber,
order.OrderDate,
order.Quantity,
order.Amount
);
}
}

LINQ to SQL
Microsoft OLE DB Provider for Jet 可用來存取及查詢文字檔,但必須在文字檔的相同目錄中建立 Schema.ini 檔來描述文字檔的結構:
[orders.txt]
Format=FixedLength
ColNameHeader=False
MaxScanRows=0
Col1=StoreID Text Width 4
Col2=OrderNumber Text Width 6
Col3=OrderDate DateTime Width 8
Col4=Quantity Long Width 8
Col5=Amount Long Width 9
DateTimeFormat=yyyyMMdd

然後,再定義一個資料結構對應的類別:
public class Order
{
public string StoreID;
public string OrderNumber;
public DateTime OrderDate;
public int Quantity;
public int Amount;
}

使用 DataContext 物件透過資料庫連接直接執行 SQL 查詢,並傳回資料結構對應的物件集合。
string filePath = @"d:\orders.txt";
string connString = string.Format( @"Provider=Microsoft.Jet.OLEDB.4.0;" +
"Data Source={0};" +
"Extended Properties='text;HDR=No;Format=FixedLength'",
Path.GetDirectoryName(filePath));

OleDbConnection conn = new OleDbConnection(connString);
DataContext db = new DataContext(conn);

string cmdText = string.Format("SELECT * FROM {0}",
Path.GetFileName(filePath).Replace(".", "#"));
var orders = db.ExecuteQuery<Order>(cmdText);

foreach (var order in orders)
{
Console.WriteLine("{0},{1},{2},{3},{4}",
order.StoreID,
order.OrderNumber,
order.OrderDate,
order.Quantity,
order.Amount
);
}

FileHelpers Library
FileHelpers 是一個用來處理符號區隔或是固定寬度資料的免費類別庫。使用前,你首先必須先定義一個資料結構對應的類別:
[FixedLengthRecord]
public class Order
{
[FieldFixedLength(4)]
public string StoreID;

[FieldFixedLength(6)]
public string OrderNumber;

[FieldFixedLength(8)]
[FieldConverter(ConverterKind.Date, "yyyyMMdd")]
public DateTime OrderDate;

[FieldFixedLength(8)]
[FieldTrim(TrimMode.Left)]
public int Quantity;

[FieldFixedLength(9)]
[FieldTrim(TrimMode.Left)]
public int Amount;
}

然後,透過 FileHelperEngine 讀取資料檔就會自動建立對應的物件陣列。
FileHelperEngine engine = new FileHelperEngine(typeof(Order));          
Order[] orders = engine.ReadFile(@"d:\orders.txt") as Order[];

foreach (var order in orders)
{
Console.WriteLine("{0},{1},{2},{3},{4}",
order.StoreID,
order.OrderNumber,
order.OrderDate,
order.Quantity,
order.Amount
);
}


參考資料:
LINQ TO Text Files by Eric White

繼續閱讀...

使用 C# 產生網頁縮圖

0 comments
在 Web 2.0 網站中,我們常可以看到分享文章或是推薦網站的快照縮圖(Snapshot Thumbnail),來供使用者預覽。如果要在 .NET 中要實做網頁快照,你可以使用 WebBrowser 控制項來達成。在本文中將會示範如何建立 Windows 應用程式來擷取網頁快照並產生縮圖。
snap

範例程式碼
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;

namespace SnapExample
{
public partial class MainForm : Form
{
private WebPageSnapshot _wps = new WebPageSnapshot(1024, 768);

public MainForm()
{
InitializeComponent();
_wps.ImageCompleted += new ImageCompletedEventHandler(RefreshImage);
}

private void uxGo_Click(object sender, EventArgs e)
{
EnableButtons(false);
_wps.GenerateAsync(uxUrl.Text);
}

private void uxSaveAs_Click(object sender, EventArgs e)
{
uxSaveFileDialog.DefaultExt = "jpg";
uxSaveFileDialog.AddExtension = true;
uxSaveFileDialog.Filter = "JPEG(*.jpg)|*.jpg";
uxSaveFileDialog.ShowDialog(this);

string fileName = uxSaveFileDialog.FileName;
if (fileName != string.Empty)
{
uxThumbnail.BackgroundImage.Save(fileName);
}
}

private void RefreshImage(Image image)
{
decimal sizeRatio = ((decimal)image.Height / image.Width);

int thumbWidth = 100;
int thumbHeight = decimal.ToInt32(sizeRatio * thumbWidth);

Image thumb = image.GetThumbnailImage(
thumbWidth, thumbHeight,
() => false,
IntPtr.Zero
);

if (WebPageSnapshot.IsBlank((Bitmap)thumb))
{
using (Graphics g = Graphics.FromImage(thumb))
{
StringFormat format = new StringFormat();
format.Alignment = StringAlignment.Center;
format.LineAlignment = StringAlignment.Center;
g.DrawString(
"IMAGE NOT AVAILABLE",
new Font("Tahoma", 65),
Brushes.DarkGray,
new RectangleF(0, 0, thumbWidth, thumbHeight),
format
);
}
}

uxThumbnail.BackgroundImage = thumb;

EnableButtons(true);
}

private void EnableButtons(bool enabled)
{
uxSaveAs.Enabled = enabled;
}

private void uxCancel_Click(object sender, EventArgs e)
{
_wps.CancelAsync();
}
}
}

WebPageSnapshot 是一個以 WebBrowse 類別為對象的自訂外覆類別(Wrapper Class)。透過非同步呼叫 GenerateAsync 方法,會將網頁載入到內部隱含的 WebBrowser 實例中。當 WebBrowser 完整載入網頁後,便會呼叫其 DrawToBitmap 方法建立網頁快照的點陣圖,然後引發 ImageCompleted 事件傳回快照圖。在上例中,將會由 RefreshImage 程序來處理 ImageCompleted 事件,並藉由傳入的 Image 物件的 GetThumbnailImage 方法建立縮圖影像。

不過,在測試過程中發現偶會有擷取到空白影像的情況。為了能處理此例外狀況,我從這裡找來了辨識空白圖片的程式演算法,並引用到 WebPageSnapshot 類別的 IsBlank 靜態方法。在範例中,我就對空白截圖繪製了 "IMAGE NOT AVAILABLE" 的字樣。
no image

Download source code

參考資料:
Algorithm to Detect Blank Images by Chinh Do
Event-based Asynchronous Pattern Overview by Microsoft

繼續閱讀...

ASP.NET Image Uploading with Resizing

0 comments
當你使用 ASP.NET 建立上傳圖檔的應用程式時,除了要決定寫入檔案系統或是儲存到資料庫外,你可能還會需要依照原圖的尺寸比例來調整圖片的大小,以符合系統的需求。本文範例程式碼將透過 Image 及 Bitmap 類別,對上傳的原圖進行比例縮放,並以 JPEG 圖檔格式存儲。

<%@ Page Language="C#" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="Drawing=System.Drawing" %>
<script runat="server">
const string _uploadFolder = "~/images/";
const int _maxThumbWidth = 100;

protected void uxUpload_Click(object sender, EventArgs e)
{
if (uxImageFile.HasFile)
{
if (IsValidImage(uxImageFile.FileName))
{
string thumbPath = _uploadFolder +
Path.ChangeExtension(Path.GetRandomFileName(), ".jpg");

using (Drawing::Image image =
Drawing::Image.FromStream(uxImageFile.PostedFile.InputStream))
{
decimal sizeRatio = ((decimal)image.Height / image.Width);
int thumbWidth = _maxThumbWidth;
int thumbHeight = decimal.ToInt32(sizeRatio * thumbWidth);

using (Drawing::Bitmap bitmap =
new Drawing::Bitmap(image, new Drawing::Size(thumbWidth, thumbHeight)))
{
bitmap.Save(Server.MapPath(thumbPath),
Drawing::Imaging.ImageFormat.Jpeg);
}
}
uxThumbImage.ImageUrl = thumbPath;
}
else
{
// This type of file is not allowed.
uxThumbImage.ImageUrl = string.Empty;
}
}
}

public bool IsValidImage(string path)
{
return Regex.IsMatch(path, @"(.*?)\.(gif|jpg|jpeg|png)$", RegexOptions.IgnoreCase);
}
</script>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Image Upload and Resize</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:FileUpload ID="uxImageFile" runat="server" /><br/>
<asp:Button ID="uxUpload" runat="server"
Text="Upload" onclick="uxUpload_Click" /><br/>
<asp:Image ID="uxThumbImage" runat="server"
onerror="javascript:this.style.display='none';" />
</div>
</form>
</body>
</html>

參考資料:
Create High Quality Thumbnail - Resize Image Dynamically - ASP .Net C# Code

繼續閱讀...

Understanding CSS Positioning

0 comments
在網頁中,CSS 將每個元素視為一個矩形區塊(Box)。每個區塊則由內容(Content)、邊距(Padding)、邊框(Border)及邊界(Margin)等區域組合而成,這樣的概念又被稱為區塊模型(Box Model)。
box
CSS 運用區塊模型的概念,將樣式套用在元素上來決定其外觀及位置。本文接下來將介紹在 CSS 排版中不可或缺的定位(Positioning)觀念。
靜態位置(position:static)
這是所有 HTML 元素預設的 position 屬性值(也就是沒有定位),它會配合版面配置規則(Layout Rule)決定其位置座標。所以,如果在 position:static 的元素設定 top、left、right 或 bottom 等偏移(Offset)屬性將會自動被忽略。

相對位置(position:relative)
相對於原本的位置(靜態位置),也就是將元素從原先的位置,依據元素的偏移屬性,將元素作對應的距離調整,而且其原本位置的區塊空間將會被保留,位移後也可能會覆蓋其它區塊。如果有區塊相互重疊的情況,你可以使用 z-index 屬性來決定重疊的順序。


絕對位置(position:absolute)
以父元素的邊界位置為基點,再依據偏移屬性作對應的距離調整。如果其上層的親代元素都不是以 absolute 或 relative 定位時,那麼就會以 body 元素的邊界為基準。該元素會從 HTML 的正常排版(Normal Flow)中移除,然後再依據容器區塊(Containing Box)的邊界來重新定位該元素,因此也可能會覆蓋其它原塊。而所謂的正常排版是指從上而下、由左至右的編排方式。例如段落區塊(Block Box)會在其容器區塊中從上而下排列;而行內區塊(Inline Box)則是由左至右排列。


固定位置(position:fixed)
基本上與絕對位置的定位方式相同,為一的差別在於其位置固定,且不會隨捲軸的移動而改變,就像浮水印(Watermark)一樣。這個屬性值並不適用 IE 6 或更早期的版本。

參考資料:
CSS basic box model

繼續閱讀...

.NET Naming Conventions

0 comments
遵循良好的命名慣例可以讓程式更容易被理解和閱讀。尤其,在團隊工作中採用一致的命名方針將利於程式碼的整合與維護。

匈牙利命名法(Hungarian Notation)是常見的命名方式,如果你在 .NET 程式設計中一眛的沿用匈牙利命名法,不僅無助於提升程式碼的可維護性,甚至會因為命名冗雜造成反效果。果真如此,你就更應該考慮使用更好的命名方式。在這裡推薦由 Pete Brown 依據微軟的開發類別庫的設計方針及業界公認標準(Industry-Accepted Standards)所整理的 .NET 命名準則,非常具有參考價值。

值得一提的是,這份文件還特別針對 Windows 表單和 Web 表單中的 UI 控制項識別名稱,提出獨道的命名原則。雖然作者特別強調不贊成使用匈牙利命名法的原因,然而在不使用匈牙利命名法的原則下,UI 控制項的識別名稱是唯一不在此限的例外。作者建議對所有 UI 控制項的識別名稱,統一採用 "ux"(意即 User eXperience 的縮寫)做為前綴詞(Prefix),其所持的理由如下:
  1. 相較於使用型別相依的匈牙利命名法更為簡潔,且不用顧慮變更控制項型別所帶來的名稱衝突問題。
  2. 有助於在 Intellisense 中將你的控制項型別變數名稱群組在一起。

如此一來,將可擺脫以往使用繁雜的控制項前綴詞(如 txt、btn 等)的命名方式。如果你也像我一樣常為控制項識別名稱命名感到苦惱,那麼相信採用新命名法將會為你帶來更佳的表單開發經驗。

資料來源:
Common .NET Naming Conventions by Pete Brown

繼續閱讀...

Mooquee - Marquee with Mootools

2 comments
Mooquee 是一個以 MooTools 為基礎所設計的跑馬燈的 JavaScript 類別,它提供建構選項可以表現出多種不同於 Marquee 標籤的跑馬燈動畫效果。

實作時,除了要引用 Mooquee 程式庫外,你還需要搭配 MooTools 1.2。在建構 Mooquee 物件時,你可以設定以下建構選項:
  • element:指定做為跑馬燈區塊的 HTML 元素識別碼。
  • cssitem:指定播放項目的樣式類別名稱。
  • firstitem:指定第一個播放項目的索引。
  • direction:以 up、down、left 或 right 指定跑馬燈移動的方向。
  • pause:以秒為單位指定每次停止時間,且必須大於或等於 duration 的值,其預設值為 1 秒。
  • duration:以秒為單位指定每次移動所需時間,其預設值為 1 秒。
  • overflow:設定當內容超出跑馬燈區塊範圍時的處理方式,預設值為 hidden。
  • startOnLoad:指定是否自動播放,預設值為 true。
如以下範例會建立向上垂直捲動的跑馬燈:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
<title>Mooquee Example</title>
<style type="text/css">
#mooquee_container {
width: 200px;
height: 20px;
border: 1px solid;
}
.mooquee_item {
background-color: #ffffff;
}
</style>
<script type="text/javascript" src="mootools-1.2-core.js"></script>
<script type="text/javascript" src="Mooquee.js"></script>
<script type="text/javascript">
window.addEvent('domready', function() {
var mooquee = new Mooquee({
element:'mooquee_container',
cssitem:'mooquee_item',
direction:'up',
duration:1,
pause:2,
firstitem:0
});
});
</script>
</head>
<body>
<div id="mooquee_container">
<div class="mooquee_item">跑馬燈訊息第 1 則</div>
<div class="mooquee_item">跑馬燈訊息第 2 則</div>
<div class="mooquee_item">跑馬燈訊息第 3 則</div>
</div>
</body>
</html>

繼續閱讀...