2018年11月26日 星期一

簡單筆記

TryParse

decimal d = 0;
string s = "123.456";

if (decimal.TryParse(s, out d))
    MessageBox.Show(d.ToString("N1"));
else
    MessageBox.Show(s);

Excel 的 MROUND

/// <summary>
/// MROUND 函數為:傳回四捨五入為所需倍數的數字。EX.MROUND(7,5)=>5  MROUND(8,5)=>10
/// </summary>
/// <param name="number">原始數值</param>
/// <param name="multiple">倍數因子 (計算0.5的倍數就傳0.5)</param>
/// <returns>計算後的結果,效果等同 Excel MROUND 公式</returns>
public static decimal GetMROUND(decimal number, double multiple)
{
    return Math.Round(number / (decimal)multiple, MidpointRounding.AwayFromZero) * (decimal)multiple;
}

2018年9月12日 星期三

.Net 非同步處理

初次接觸 Windows Form 程式設計時,因為基本觀念不清楚,常常會鬧笑話,以下就是一個常見的例子:
為了避免某段程序執行時無回應,想用另一個 Thread 來處理並回寫資料。(通常是寫入某個元件,例如 Label 或 TextBox,同理讀取資料也是)

看似簡單,但因為 Windows Form 的 UI Thread 是分開的,往往不是只看到最後結果,就是在不同 Thread 內呼叫彼此 UI 元件跳出異常 (跨 Thread 存取 UI 元件是不允許的)。找了一些資料看,程度太差,也是一直半解 Task、Backgroudworker、BeginInvoke、Delegate...

直到發現這篇,寫得非常好:
https://dotblogs.com.tw/johnny/2014/04/02/144594

其中有一段寫到
... 因為上面那三行程式都在同一個 UI thread 上面, 而 Windows Form 會在 UI thread 上的指令執行完畢並且返回之後, 才會觸發它的 Paint 事件。同樣的程式, 如果 ... 在 Console 程式中這個問題就不會發生, 因為 Console 程式沒有像 Windows Form 那種 Paint 事件。

上面的連結寫得很詳細,不再贅述,簡單紀錄一下自己的用法如下:

private async void btnTest_Click(object sender, EventArgs e)
{
    //
    // do something...
    //

    await doSomethingAsync();
}

private async Task doSomethingAsync()
{
    //
    // do something...
    //

    await Task.Delay(1000); // 暫停一下,看效果用
}

2018年9月11日 星期二

利用 Trigger 紀錄資料表異動 (Log)


原始資料表:[xxx]
異動資料表:[xxx_log]

[xxx_log] 比 [xxx] 至少要多兩個欄位:

  • T1,異動時間,Datetime,預設 getdate() 即可。
  • T2,異動指令,Varchar(10),存放 INSERT UPDATE DELETE 做為識別。

2019/03/25 補充
兩個系統資料表 [INSERTED] & [DELETED]
執行指令 insert 後,[INSERTED] 表內有新增的資料,[DELETED] 表內無資料。
執行指令 delete 後,[INSERTED] 表內無資料,[DELETED] 表內有刪除的資料。
執行指令 update 後,[INSERTED] 表內有更新的資料,[DELETED] 表內有更新的資料。
所以執行指令後,可藉由 select 兩個表判斷為哪種行為,並取得資料。
 
建立一個名為 tr_xxx 的 Trigger

CREATE TRIGGER tr_xxx ON [dbo].[xxx] --// 在資料表 xxx 建立一個名為 tr_xxx 的 Trigger
AFTER INSERT, UPDATE --// 如果需要判斷 DELETE 得加進來
AS

SET NOCOUNT ON; --// 這行是為了正確紀錄批次 UPDATE 或 DELETE,如果不寫,只會 LOG 到一筆異動
DECLARE @INS int, @DEL int --// 取得 INSERTED 與 DELETED 的資料數

--// INSERT:@INS > 0 AND @DEL = 0
--// DELETE:@INS = 0 AND @DEL > 0
--// UPDATE:@INS > 0 AND @DEL > 0

SELECT @INS = COUNT(*) FROM INSERTED
SELECT @DEL = COUNT(*) FROM DELETED

IF @INS > 0 AND @DEL > 0 
BEGIN
     INSERT INTO [xxx_log]  ( T2, C1, C2, C3 ... )  
         SELECT 'UPDATE', C1, C2, C3 ... FROM INSERTED
END

ELSE 
BEGIN
     INSERT INTO [xxx_log]  ( T2, C1, C2, C3 ... )  
         SELECT 'INSERT', C1, C2, C3 ... FROM INSERTED
END

參考資料:
https://stackoverflow.com/questions/9931839/create-a-trigger-that-inserts-values-into-a-new-table-when-a-column-is-updated
https://dotblogs.com.tw/jamesfu/2014/06/20/triggersample

2018年9月10日 星期一

ComboBox 禁用滑鼠滾輪

ComboBox 點選後,焦點會停留在元件上,此時如動到滑鼠滾輪,可能會造成資料變動。

最初的解決方像是想變更焦點,因為 Winform 沒有類似 Blur() 的函式,所以在 SelectedIndexChanged 事件加入:
// 下拉選單選擇後就讓焦點消失,避免USER滾到滑鼠滾輪
this.ActiveControl = null;


但是,如果 USER 這樣操作:點了選單,但並沒有改變選項,又再次點擊選單本身,滾輪依然是有效的。想在 MouseLeave 內處理,但是同上,沒有離開 (Leave) 也是無效。

上述的方式換成讓其他元件 Focus() 也可以,問題在於沒有合適的事件能處理。最後只好新建一個 ComboBox 類別,繼承原生物件,但屏蔽滑鼠滾論事件。
using System.Windows.Forms;

namespace LIB.MyControl
{
    // 定義一個新的 ComboBox,繼承原本元件,但排除滾論事件 (m.Msg == 0x020A)
    public class ComboBoxNoWheel : System.Windows.Forms.ComboBox, IMessageFilter
    {
        public ComboBoxNoWheel()
        {
            Application.AddMessageFilter(this); // 整個 Application 都會有影響
        }

        public bool PreFilterMessage(ref Message m)
        {
            if (m.Msg == 0x020A && this.Focused) // 加入 this.Focused,判斷 ComboBox 為焦點時才作用,避免影響整個 Application
                return true;
            return false;
        }
    }
}


參考資料:
https://blog.csdn.net/jamex/article/details/4257679

2018年9月7日 星期五

取得軟體版本編號 ( 含 ClickOnce )

一般 Winform 自行修改「組件資訊」內的版本編號
// 取得組件資訊內的版本
AssemblyName.GetAssemblyName(System.Windows.Forms.Application.ExecutablePath).Version.ToString();

使用 ClickOnce 發行的版本編號
// 判斷是否為發行版本 (否則 Debug 執行時會出錯)
if (System.Deployment.Application.ApplicationDeployment.IsNetworkDeployed)
{
    MessageBox.Show(System.Deployment.Application.ApplicationDeployment.CurrentDeployment.CurrentVersion.ToString());
}

2018年8月28日 星期二

Winform 軟體自動更新

Winform 的更新方式很多,最方便的莫過於微軟的 ClickOnce。這次的需求無法用這個機制滿足User,只好自己開發,順便紀錄一下。

網路上能找了幾篇來參考:



最後整理出一些重點符合需求也能兼顧效能與彈性,我的版本:


  1. 更新檔直接放共享網路。(不用架設額外 Web 或 FTP,大家也都能存取)
  2. 更新檔封裝為一個壓縮檔,縮短下載(複製)時間。
  3. 放置一個純文字文件紀錄目前版本供本地端讀取比較。
  4. 更新時不需出現確認視窗,有更新就直接執行。(ClickOnce會出現)
  5. 需於主程式啟動判斷更新,並可定時檢查與手動執行。

簡單畫個圖 (包含專案佈署流程):

2018年8月21日 星期二

INSERT INTO SELECT 與 SELECT INTO FROM

INSERT INTO SELECT

INSERT INTO TableB ( field1, field2... ) SELECT field1, field2... FROM TableA

  • 目標 TableB 必須已存在。
  • 類似一般 INSERT INTO VALUES 寫法,只是來源改為 TableA
  • 如果 TabelB 與 TableA 結構(含欄位順序)完全一致,後半段可用 * 取代欄位名稱。


SELECT INTO FROM

SELECT field1, field2... INTO TableB FROM TableA

  • 目標 TabelB 不存在。
  • 執行語法時會自動建立資料表並加入值。

2018年8月16日 星期四

偵測程式是否仍在執行,重新啟動並 Email 通知

最近因為工作需要,想設計一個機制:

  1. 持續檢查某個程序是否仍在執行。
  2. 若發現停止執行,重新啟動。
  3. 發現問題時以 Email 通知。

Google 了一下發現兩篇:參考一 參考二

第一個範例已經相當完整,指令模式下寄信使用了 SendEmail

流程圖:

2018年7月23日 星期一

IIS7 執行 ASP

IIS的安裝項目與設定可參考:
http://isvincent.pixnet.net/blog/post/30715336-%E5%9C%A8iis-7%E4%B8%AD%E4%BD%BF%E7%94%A8asp%E7%A8%8B%E5%BC%8F

AppPool的建立與設定可參考:
https://dotblogs.com.tw/gelis/2014/12/13/147627

之後在IIS內建立應用程式並設定AppPool應該就可以了

2018年7月11日 星期三

MDI 架構,子表單存取父層表單元件



一般子表單存取父表單的元件資料,通常會這樣寫。

Form1 (父),內含一個 TextBox1
Form2 (子),想存取 TextBox1

Form1 這樣寫:

Form2 frm = new Form2(this); //設定Owner 
frm.ShowDialog();


Form2 這樣寫:
TextBox txt = ((Form2)Owner).TextBox1; //取得Owner內元件


但如果今天放到MDI架構下會不太一樣,假設今天....


FormMain 設定為 MdiContainer,從裡面啟動 Form1
Form1 (父),內含一個 TextBox1
Form2 (子),想存取 TextBox1

 這時如果同上述寫法,Form2 的 Owner (居然)會抓到 FormMain !?
 Form2 必須改為這樣寫:
TextBox txt = ((Form2)Owner.ActiveMdiChild).TextBox1;

2018年7月9日 星期一

SQL Datetime Convert Format (日期格式轉換)

常用,但是每次都要Google,備份一下。

參考來源

- Microsoft SQL Server T-SQL date and datetime formats
- Date time formats - mssql datetime 
- MSSQL getdate returns current system date and time in standard internal format
SELECT convert(varchar, getdate(), 100) - mon dd yyyy hh:mmAM (or PM)
                                        - Oct  2 2008 11:01AM          
SELECT convert(varchar, getdate(), 101) - mm/dd/yyyy 10/02/2008                  
SELECT convert(varchar, getdate(), 102) - yyyy.mm.dd - 2008.10.02           
SELECT convert(varchar, getdate(), 103) - dd/mm/yyyy
SELECT convert(varchar, getdate(), 104) - dd.mm.yyyy
SELECT convert(varchar, getdate(), 105) - dd-mm-yyyy
SELECT convert(varchar, getdate(), 106) - dd mon yyyy
SELECT convert(varchar, getdate(), 107) - mon dd, yyyy
SELECT convert(varchar, getdate(), 108) - hh:mm:ss
SELECT convert(varchar, getdate(), 109) - mon dd yyyy hh:mm:ss:mmmAM (or PM)
                                        - Oct  2 2008 11:02:44:013AM   
SELECT convert(varchar, getdate(), 110) - mm-dd-yyyy
SELECT convert(varchar, getdate(), 111) - yyyy/mm/dd
SELECT convert(varchar, getdate(), 112) - yyyymmdd
SELECT convert(varchar, getdate(), 113) - dd mon yyyy hh:mm:ss:mmm
                                        - 02 Oct 2008 11:02:07:577     
SELECT convert(varchar, getdate(), 114) - hh:mm:ss:mmm(24h)
SELECT convert(varchar, getdate(), 120) - yyyy-mm-dd hh:mm:ss(24h)
SELECT convert(varchar, getdate(), 121) - yyyy-mm-dd hh:mm:ss.mmm
SELECT convert(varchar, getdate(), 126) - yyyy-mm-ddThh:mm:ss.mmm
                                        - 2008-10-02T10:52:47.513

- SQL create different date styles with t-sql string functions
SELECT replace(convert(varchar, getdate(), 111), -/-, - -) - yyyy mm dd
SELECT convert(varchar(7), getdate(), 126)                 - yyyy-mm
SELECT right(convert(varchar, getdate(), 106), 8)          - mon yyyy

2018年6月12日 星期二

PDF 隱藏工具列

現今一般瀏覽器要檢視PDF已經不是問題,最直覺的方式就是塞在iframe。

<iframe id = "iframe1" runat="server" src="MyFile.pdf">
</iframe>

那有沒有可能進一步做到控制工具列顯示或隱藏呢?

找了一下資料發現了這個:Parameters of Opening PDF Files



上述的連結,我已經在結尾加入了 #page=7
有沒有發現打開不是在第一頁? :D

也就是說最開頭的iframe部分,我們可以改寫為:

<iframe id = "iframe1" runat="server" src="MyFile.pdf#toolbar=0">
</iframe>

開啟PDF時就不會看到工具列了,我測試IE與Chrome有效。

補充說明:

  • 參數可能因瀏覽器或Acrobat版本不同出現差異。
  • 也可使用一些JS套件,避免上述問題,例如 pdf.jsPdfObject

DevExpress 處理 Word 轉圖檔 (以及PDF、HTML)

最近的需求要實作「Word轉圖檔」,研究一些相關技術後,還是使用元件方便。公司剛好有買DevExpress,剛好拿來練習....

網路上找到的範例:

直接複製不能用,可能是版本不同,修改結果如下:

string pathSource = Server.MapPath("~/Files/Sample.docx");
            string pathTarget = Server.MapPath("~/Export/Image/Sample.png");

            RichEditDocumentServer sourceServer = new RichEditDocumentServer();
            sourceServer.LoadDocument(pathSource);

            PrintingSystemBase ps = new PrintingSystemBase();
            PrintableComponentLinkBase pLink = new PrintableComponentLinkBase(ps);
            pLink.Component = sourceServer;
            pLink.CreateDocument(true);

            ImageExportOptions options = new ImageExportOptions();
            options.Format = ImageFormat.Png; // 可輸出為不同圖檔格式
            options.Resolution = 300;
            options.ExportMode = ImageExportMode.DifferentFiles; //可選擇輸出單圖或多圖

            pLink.ExportToImage(pathTarget, options);

DevExpress要參考的dll不少,Web.config可能也需要組態設定,可以直接開DevExpress專案開發,或者新開一個專案參考內容再回頭修改原本專案。

開啟DX專案會自動引用:


using部分


補充:

2018年6月11日 星期一

使用 Regex.Replace 快速取代字串

這次需求是需要取出地址前兩個字,但資料各種組成都有,例如:

(799)臺南市仁德區保安路9段99號~90
799臺南市仁德區保安里99鄰保安路9段99號~90
799-00臺南市仁德區保安里99鄰保安路9段99號~90
(79988)臺南市仁德區保安里99鄰保安路9段99號~90@!?+=-\|%$^#*&{}[]

使用 Regex.Replace先將數字與特殊字元取代(主要處理郵遞區號部分)

string address = @"(79988)臺南市仁德區保安里99鄰保安路9段99號~90@!?+=-\|%$^#*&{}[]";
string pattern = @"[\d\W_]";
string replacement = "";
Regex rgx = new Regex(pattern);
string result = rgx.Replace(address, replacement);


結果:

臺南市仁德區保安里鄰保安路段號

PhotoUtils.cs 找不到

引用DevExpress範例時遇到的問題,路徑可參考這篇:

https://www.devexpress.com/Support/Center/Question/Details/Q535762/the-devexpress-web-demos-photoutils-class-is-missing

官方說法在:
c:\Users\Public\Documents\DXperience 13.1 Demos\ASP.NET\CS\ASPxEditorsDemos\App_Code\PhotoUtils.cs

版本不同可能有差異,我的是在這裡找到:
C:\Users\Public\Documents\DevExpress Demos 16.2\Components\ASP.NET\CS\ASPxEditorsDemos\App_Code



加入後記得引用:
using DevExpress.Web.Demos;

2018年6月6日 星期三

錯誤 CS2001 \Properties\AssemblyInfo.cs could not be found.

開啟專案後 Properties/AssemblyInfo.cs 顯示問號

建置顯示錯誤
Source file 'D:\SampleCode\MsWordToImage\MsWordToImage\Properties\AssemblyInfo.cs' could not be found.




參考
https://stackoverflow.com/questions/6494691/source-file-properties-assemblyinfo-cs-could-not-be-found

屬性 > 應用程式 > 組件資訊 (即AssemblyInfo)




打開應該會是一片空白,請(至少)填入標題 (Title),按下確定。

這時問號就會消失(組件資訊已經產生),其他資訊系統會自動寫入,結果如下圖。


2018年6月5日 星期二

WITH (NOLOCK) 實作與驗證

建了一個USER資料表,包含ID與金額欄位




撰寫一個簡單的TRANSACTION,更新ID=3的USER金額




Transaction有正常begin與commit,所以再次select時,資料正確。




再來我故意將commit拿掉,此時Transaction執行後將無法結束(也就是TABLE會被LOCK)
從參數 @@TRANCOUNT知道此TRAN已經執行了(一次),但因為沒有commit,資料都還沒真的寫入。




這時候在回頭執行SELECT時,會撈不出資料




但是!
如果此時加入 WITH (NOLOCK),就可以撈出來了!!
可以看到值也已經更改,因為UPDATE有執行到 (請記得資料還沒COMMIT)




這時候回頭加入rollback,再執行一次transaction,
資料會復原,TABLE也會解除LOCK




此時一般的SELECT就可以執行了,不會再TIMEOUT
而數值會恢復到執行前的狀態(因為資料rollback了)




所以如果某個系統程序執行了一段冗長耗時的TRANSACTION
所有相關的TABLE在執行期間都會LOCK,此時某人要撈資料就必須下WITH (NOLOCK)

以上例子:
  • 金額amount會在Transaction內異動,不適合搭配NOLOCK撈取。
  • ID或姓名name,可使用NOLOCK,避免Transaction執行間無法撈取。


附帶一提,如果剛好要撈的資料欄位必須等TRANSACTION結束(不能用NOLOCK),
又不想讓USER看到逾時的話,可以改用NOWAIT判斷,然後提示USER,如下:




關於 NOWAIT 的實作範例可 參考這裡

2018年4月8日 星期日

自動送出 Enter

在處理 DataGridView 編輯時,原本使用 GetChanges() 方法,取得 DataTable 中有異動的資料。 再根據其 RowState 決定要 INSERT 或 UPDATE。 但如果使用者編輯後沒有按TAB或ENTER,呼叫 GetChangs() 可能會得到 null 類似這篇 作者是參考這篇解決 測試後依然無解,最後乾脆於儲存時強行 ENTER
SendKeys.SendWait("{ENTER}");
參考這篇,送出字串與按鈕的方法不同。

DataGridView 內使用 DateTimePicker

宣告全域變數
private DateTimePicker cellDateTimePicker;
初始化 Form1() 時加入
this.cellDateTimePicker = new DateTimePicker();
this.cellDateTimePicker.ValueChanged += new EventHandler(cellDateTimePicker_ValueChanged);
//this.cellDateTimePicker.CloseUp += new EventHandler(cellDateTimePicker_CloseUp);
this.cellDateTimePicker.Format = DateTimePickerFormat.Short;
this.cellDateTimePicker.ShowCheckBox = true;
this.cellDateTimePicker.Checked = true;
this.cellDateTimePicker.Visible = false;
this.dgvCertificate.Controls.Add(cellDateTimePicker);
在 ValueChanged 事件加入對應方法 (cellDateTimePicker_ValueChanged),一些屬性設定。 最後將 DateTimePicker 加入 DataGridView 的 Controls 內 撰寫 cellDateTimePicker_ValueChanged
void cellDateTimePicker_ValueChanged(object sender, EventArgs e)
{
    if (cellDateTimePicker.Checked)
    {
        //cellDateTimePicker.Format = DateTimePickerFormat.Short;
        //cellDateTimePicker.CustomFormat = null;
        dgvCurrent.CurrentCell.Value = cellDateTimePicker.Value.ToString("yyyy/MM/dd"); //convert the date as per your format
    }
    else
    {
        //cellDateTimePicker.Checked = false; // 採用checkbox,清除值要取消勾選
        //cellDateTimePicker.Format = DateTimePickerFormat.Custom;
        //cellDateTimePicker.CustomFormat = " ";
        //cellDateTimePicker.forma
        dgvCurrent.CurrentCell.Value = " ";
    }
    cellDateTimePicker.Visible = false;
}
在 DataGridView 的 CellBeginEdit 事件內加入判斷,對應的儲存格才用DateTimePicker取代
private void dgvCertificate_CellBeginEdit(object sender, DataGridViewCellCancelEventArgs e)
{
    dgvCurrent = dgvCertificate;
    // 如果欄位是證照日期就用之前宣告的 cellDateTimePicker 取代
    // 也可以用  e.ColumnIndex == 1 判斷,用 Name 避免欄位移動又得修改
    if ("certificate_CER_DATE".Equals(dgvCertificate.Columns[e.ColumnIndex].Name))
    {
        Rectangle tempRect = dgvCertificate.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, true);
        cellDateTimePicker.Location = tempRect.Location;
        cellDateTimePicker.Width = tempRect.Width;
        string s = dgvCurrent.CurrentCell.Value.ToString();
        cellDateTimePicker.Value = (!"".Equals(s) && !"0001/1/1 上午 12:00:00".Equals(s)) ? DateTime.Parse(s) : DateTime.Today;
        cellDateTimePicker.Checked = (!"".Equals(cellDateTimePicker.Value) && !"0001/1/1 上午 12:00:00".Equals(cellDateTimePicker.Value)) ? true : false;
        cellDateTimePicker.Visible = true; //這行要放在所有設定之後
    }
}
編輯結束後隱藏 DateTimePicker
private void dgvCertificate_CellEndEdit(object sender, DataGridViewCellEventArgs e)
{
    if ("certificate_CER_DATE".Equals(dgvCertificate.Columns[e.ColumnIndex].Name))
    {
        cellDateTimePicker.Visible = false;
    }
    //MessageBox.Show(dgvCertificate.Rows[e.RowIndex].Cells[e.ColumnIndex].Value.ToString()+
    //    dgvCertificate.Rows[e.RowIndex].Cells[e.ColumnIndex].FormattedValue.ToString());
}