2018-01-30

リンクカード(ブログカード)を作るブックマークレット

リンクの見栄えを良くしたくて、以下のようなリンクカード(ブログカード)を探していました。

  1. 外部サービスに依存しない。(サービスの継続性の問題とパフォーマンスの点から)
  2. OGP (Open Graph Protocol) に対応していないサイトへのリンクはimagedescriptionを無理に表示しない。(関係のない画像やコンテンツが表示されることがあるので)
  3. 仕様が開示されている。(カスタマイズ可能)
  4. StackEdit の Markdownで利用可能。(シンプルなHTMLであること)

なかなか良いものが見つからず、<blockquote> で代用したりしていたのですが、以下のサイトを見つけました。

はてな風のブログカードをブックマークレットから作ってみよう! | 株式会社グランフェアズ

こんにちは、めぐたんです。 ブログを書いていると、参考記事や過去に書いた記事など別ページへのリンクを貼る機会が何かと多くあります。…

 (これはリンク先のブックマークレットで作ったリンクカードです。)

詳しくはリンク先を見ていただければ分かるのですが、外部サービスへの依存がほとんどなく(favicon 表示のみ)、そのため表示も高速です。

画像も og:img のみの表示です。画像がない場合も違和感のないデザインです。

ブックマークレット(JavaScript)ですので使いやすくカスタマイズも可能です。

また、使用しているタグも <div> のみですので、ブックマークレットから出力された HTML をそのまま StackEdit に貼り付けて使うことができます。

カスタマイズ

そのままでも充分使えるのですが、ブックマークレットの JavaScript を少しカスタマイズしてみます。

タイトル

obj.title = $('title').text();

でタイトルを取得しているのですが、document.title 以外に title 要素を使ったものが引っかかることがある(Qiitaなど)ので、

obj.title = document.title;

にしておきます。

テキスト部分

'<p>' + obj.desc + '</p>'

<meta name ="description" content=""> の内容を表示しているのですが、description がないサイトへのリンクだと、undefinedと表示されてしまいます。undefined の場合、自分で説明を上書きすれば良いのですが、以下のようにしてundefined を一旦消すことにします。

'<p>' + (obj.desc || '') + '</p>'

リンク

私の場合、内部リンクは別ウィンドウで開きたくないので、target="_blank" は消しておきます。(テキスト部分と画像部分の2箇所。)

URL

URL とホスト名は canonical 属性の設定があれば、そちらを使うようにします。

obj.url = document.URL;
obj.domain = location.host;

obj.url = $('link[rel=canonical]').attr('href') || document.URL;
var m = obj.url.match(/^https?:\/\/([^/]+)/);
obj.domain = m[1];

に変更します。

favicon

favicon は Google の API を使って取得しているのですが、Google は http / https 間の接続を認めていないので、"http://www.google.com/""//www.google.com/"に変更しておきます。

CSS

CSSも少しだけいじりました。faviconpadding が継承されていたので 0 をセットしたのと、favicon 右のドメインが下付きになっていたので、vertical-align: middle; を追加しました。

.blogCardFooter a img {
margin-right: 5px;
padding: 0;
vertical-align: middle;
}

修正後のブックマークレット

ブログカード


株式会社グランフェアズ様、とても役立つ情報ありがとうございました。

2018-01-19

oo4oからADOへの変換 (7) Adapterクラスの作成(ADOでパラメータの名前によるバインドを可能にする)

移行方針について

oo4o から ADO へ移行する場合、大きく二つの方針が考えられると思います。
  1. 既存コードに手に入れず、oo4o のインタフェースを実装した ADO(ADO.NET)のラッパークラスを作成する。
  2. 全面的に ADO(ADO.NET)に書き換える。
まず、手っ取り早く (1) を検討したくなります。既存コードをそのまま利用できるのですから。しかし、今後もそのコードを継続して使用する場合、廃止された仕様に縛られ続けることにもなります。また、ADO とoo4o の仕様の差は大きく、 oo4o のインタフェースを完全に実装したラッパークラスの作成は困難です。
かといって、(2) の場合は書き換えに要する時間と費用の問題があります。oo4o と ADO の仕様の差は大きく書き換えも単純ではありません。(コンバータの作成を考えましたが、文法が大きく異なるため中途半端なものにならざるを得ません。)

では、どうするのか

移行に関係なく、データプロバイダの API を素のまま使わずデータベースアクセス用の共通クラスや関数を作成して手続きを単純化することは、よくある話ですし、望ましいことです。
その共通クラスの作成の際、oo4o の仕様を織り込むことで移行コストを抑えつつ、メンテナンス性も維持することを考えてみたいと思います。

ADO ラッパー(Adapter)クラスの作成

以下のような方針で ADO のラッパークラスを作成してみます。
  • 単一クラスとする。( Excel や Access のファイルに簡単に織り込めるのが望ましい。)
  • oo4o の OraSession、OraDatabase のインタフェースを極力実装する。
  • OraDynaset は対象外(ADO.Recordset に書き換える。)
  • Oracle のデータ型を使えるようにする。
  • 「名前によるバインド」を ADO でも可能にする。
クラス名は OraAdapter とします。
OraAdapter
Option Explicit
Public Enum OraParm
ORAPARM_INPUT = 1
ORAPARM_OUTPUT = 2
ORAPARM_BOTH = 3
End Enum
Public Enum OraType
ORATYPE_VARCHAR2 = 1
ORATYPE_NUMBER = 2
ORATYPE_DATE = 12
End Enum
Public Enum OraDyn
ORADYN_DEFAULT = &H0&
ORADYN_READONLY = &H4&
ORADYN_NOCACHE = &H8&
End Enum
Private cnn As ADODB.Connection
Private cmd As ADODB.Command
' Collection of ADODB.Parameter
Public Parameters As Collection
Public Enum RefreshParameterDirection
ParamInput
ParamOutput
End Enum
Private Sub Class_Initialize()
Set cnn = New ADODB.Connection
'If you want to open database in constructor
'OpenDatabase "ExampleDb", "scott/tiger"
End Sub
Private Sub Class_Terminate()
CloseDatabase
End Sub
' OraSession.OpenDatabase
Public Sub OpenDatabase(ByVal database_name As String, ByVal connect_string As String)
Dim userPassword As Variant
userPassword = Split(connect_string, "/")
With cnn
' .Provider = "MSDAORA"
.Provider = "OraOLEDB.Oracle"
.Properties("Data Source") = database_name
.Properties("User ID") = userPassword(0)
.Properties("Password") = userPassword(1)
.Open
End With
If cmd Is Nothing Then
Set cmd = New ADODB.Command
cmd.ActiveConnection = cnn
cmd.CommandType = adCmdText
End If
End Sub
' OraSession.Close
Public Sub CloseDatabase()
ClearParameters
If Not cmd Is Nothing Then
Set cmd = Nothing
End If
If Not cnn Is Nothing Then
If cnn.State = adStateOpen Then cnn.Close
End If
End Sub
' OraDatabase.ConnectionOK
Public Property Get ConnectionOK() As Boolean
ConnectionOK = (cnn.State = adStateOpen)
End Property
' OraDatabase.Paramters.Add
Public Sub AddParameter(ByVal name As String, ByVal value As Variant, ByVal ioType As Long, Optional ByVal serverType As Long)
Dim param As ADODB.Parameter
Set param = cmd.CreateParameter(name, , ioType, , value)
If serverType = 0 Then
Select Case VarType(Value)
Case vbInteger, vbLong, vbSingle, vbDouble, vbCurrency, vbDecimal
serverType = OraType.ORATYPE_NUMBER
Case vbDate
serverType = OraType.ORATYPE_DATE
Case vbString, vbNull
serverType = OraType.ORATYPE_VARCHAR2
End Select
End If
SetParamType param, serverType
' Store into Collection
If Parameters Is Nothing Then
Set Parameters = New Collection
End If
Parameters.Add param, name
End Sub
' OraDatabase.Paramters(name).ServerType
Public Sub SetParameterServerType(ByVal name As String, ByVal serverType As Long)
SetParamType Parameters(name), serverType
End Sub
' Set the parameter type and size from Oracle ServerType
Private Sub SetParamType(ByVal param As ADODB.Parameter, ByVal serverType As Long)
Select Case serverType
Case ORATYPE_NUMBER
param.Type = adNumeric
Case ORATYPE_DATE
param.Type = adDBTimeStamp
Case ORATYPE_VARCHAR2
param.Type = adVarChar
param.SIZE = 255
End Select
End Sub
' OraDatabase.Paramters(name).Remove
Public Sub RemoveParameter(ByVal name As String)
Parameters.Remove name
End Sub
' Clear Parameters
Public Sub ClearParameters()
If Not Parameters Is Nothing Then
Set Parameters = Nothing
End If
End Sub
' Synchronize parameter values between Me.Parameters and cmd.Parametes
Public Sub RefreshParameters(Optional ByVal direction As RefreshParameterDirection = ParamInput)
Dim cmdParam As ADODB.Parameter
For Each cmdParam In cmd.Parameters
Select Case direction
Case ParamInput
cmdParam.value = Parameters(cmdParam.name).value
Case ParamOutput
Parameters(cmdParam.name).value = cmdParam.value
End Select
Next cmdParam
End Sub
' OraDatabase.CreateDynaset
Public Function CreateDynaset(ByVal sql As String, Optional ByVal options As Long = &H0&, Optional ByVal cursorName As String) As ADODB.Recordset
Dim cursorType As ADODB.CursorTypeEnum
Dim lockType As ADODB.LockTypeEnum
Dim rst As ADODB.Recordset
Set rst = New ADODB.Recordset
rst.CursorLocation = adUseClient
'Convert CursorType
If options And ORADYN_NOCACHE Then
cursorType = adOpenForwardOnly
Else
cursorType = adOpenStatic
End If
'Convert LockType
If options And ORADYN_READONLY Then
lockType = adLockReadOnly
Else
lockType = adLockOptimistic
End If
cmd.CommandText = ""
Call BindByPosition(sql, cursorName)
cmd.CommandText = sql
If cursorName <> "" Then
cmd.Properties("PLSQLRSet") = True
Else
cmd.Properties("PLSQLRSet") = False
End If
rst.Open cmd, , cursorType, lockType
Set CreateDynaset = rst
End Function
' OraDatabase.CreatePLSQLDynaset
Public Function CreatePLSQLDynaset(ByVal sql As String, ByVal cursorName As String, Optional ByVal options As Long = &H0&) As ADODB.Recordset
Set CreatePLSQLDynaset = CreateDynaset(sql, options, cursorName)
End Function
' OraDatabase.ExecuteSQL
Public Function ExecuteSQL(ByVal sql As String) As Long
Dim recordAffected As Long
Dim parametersCount As Long
cmd.CommandText = ""
parametersCount = BindByPosition(sql)
cmd.CommandText = sql
cmd.Execute recordAffected
ExecuteSQL = recordAffected
'Update values of out parameters
If parametersCount > 0 Then
RefreshParameters ParamOutput
End If
End Function
' Bind parameters by position
Private Function BindByPosition(ByRef sql As String, Optional cursorName As String) As Long
' Initialize cmd.Parameters
Dim i As Long
For i = cmd.Parameters.COUNT - 1 To 0 Step -1
cmd.Parameters.Delete i
Next
' Extract bind variables from sql
Dim reg As Object
Set reg = CreateObject("VBScript.RegExp")
With reg
.Pattern = ":(\w+)"
.IgnoreCase = True
.Global = True
End With
Dim match As Object
Dim matches As Object
Set matches = reg.Execute(sql)
BindByPosition = matches.COUNT
If matches.COUNT = 0 Then
Exit Function
End If
Dim param As ADODB.Parameter
For Each match In matches
'Add parameters to cmd.Parameters except cursor variables
If match.submatches(0) <> cursorName Then
Set param = Parameters(match.submatches(0))
'Add a deep copy of the parameter
cmd.Parameters.Append CopyParameter(param)
End If
Next match
' When sql includes cursor valiables
If cursorName <> "" Then
sql = ReplaceRefCursor(sql, cursorName)
End If
' Convert parameter markers into "?"
sql = reg.Replace(sql, "?")
End Function
' Create a deep copy of the parameter
Private Function CopyParameter(ByVal param As ADODB.Parameter) As ADODB.Parameter
Dim copy As ADODB.Parameter
Set copy = New ADODB.Parameter
With copy
.name = param.name
.value = param.value
.direction = param.direction
.Type = param.Type
.SIZE = param.SIZE
End With
Set CopyParameter = copy
End Function
' Replace sql having cursor variables
Private Function ReplaceRefCursor(ByVal sql As String, ByVal cursorName As String) As String
Dim reg As Object
Set reg = CreateObject("VBScript.RegExp")
With reg
.Pattern = ",? *:" & cursorName
.IgnoreCase = True
.Global = True
End With
' Remove cursor variables from sql
sql = reg.Replace(sql, "")
' Convert to ODBC syntax
reg.Pattern = "begin (.*); *end;"
sql = reg.Replace(sql, "{CALL $1}")
ReplaceRefCursor = sql
End Function
' OraSession.BeginTrans
Public Sub BeginTrans()
cnn.BeginTrans
End Sub
' OraSession.CommitTrans
Public Sub CommitTrans()
cnn.CommitTrans
End Sub
' OraSession.Rollback
Public Sub Rollback()
cnn.RollbackTrans
End Sub
' OraSession.ResetTrans
Public Sub ResetTrans()
On Error Resume Next
cnn.RollbackTrans
End Sub
' OraDatabase(OraSession).LastServerErr
Public Property Get LastServerErr() As Long
If cnn.Errors.COUNT > 0 Then
LastServerErr = cnn.Errors(cnn.Errors.COUNT - 1).NativeError
End If
End Property
' OraDatabase(OraSession).LastServerErrText
Public Property Get LastServerErrText() As String
If cnn.Errors.COUNT > 0 Then
LastServerErrText = cnn.Errors(cnn.Errors.COUNT - 1).Description
End If
End Property
' OraDatabase(OraSession).LastServerReset
Public Sub LastServerErrReset()
cnn.Errors.CLEAR
End Sub
view raw OraAdapter.cls hosted with ❤ by GitHub

使用例

OraAdapter クラスを使って Oracle® Objects for OLE開発者ガイドの OraParametersコレクション Addメソッド の例を書き換えてみます。
Sub Form_Load()
'Declare variables
'Dim OraSession As OraSession
'Dim OraDatabase As OraDatabase
Dim OraDatabase As OraAdapter
'Create the OraSession Object.
'Set OraSession = CreateObject("OracleInProcServer.XOraSession")
Set OraDatabase = New OraAdapter
'Create the OraDatabase Object.
'Set OraDatabase = OraSession.OpenDatabase("ExampleDb", "scott/tiger", 0&)
OraDatabase.OpenDatabase "ExamleDb", "scott/tiger"
'Add EMPNO as an Input/Output parameter and set its initial value.
'OraDatabase.Parameters.Add "EMPNO", 7369, ORAPARM_INPUT
OraDatabase.AddParameter "EMPNO", 7369, ORAPARM_INPUT
'OraDatabase.Parameters("EMPNO").serverType = ORATYPE_NUMBER
OraDatabase.SetParameterServerType "EMPNO", ORATYPE_NUMBER
'または、OraDatabase.Parameters("EMPNO").Type = adNumeric
'Add ENAME as an Output parameter and set its initial value.
'OraDatabase.Parameters.Add "ENAME", 0, ORAPARM_OUTPUT
OraDatabase.AddParameter "ENAME", 0, ORAPARM_OUTPUT
'OraDatabase.Parameters("ENAME").serverType = ORATYPE_VARCHAR2
OraDatabase.SetParameterServerType "ENAME", ORATYPE_VARCHAR2
'または、OraDatabase.Parameters("ENAME").Type = adVarChar
'OraDatabase.Parameters("ENAME").Size = 255
'Add SAL as an Output parameter and set its initial value.
'OraDatabase.Parameters.Add "SAL", 0, ORAPARM_OUTPUT
OraDatabase.AddParameter "SAL", 0, ORAPARM_OUTPUT
'OraDatabase.Parameters("SAL").serverType = ORATYPE_NUMBER
OraDatabase.SetParameterServerType "SAL", ORATYPE_NUMBER
'または、OraDatabase.Parameters("SAL").serverType = adNumeric
'Execute the Stored Procedure Employee.GetEmpName to retrieve ENAME.
' This Stored Procedure can be found in the file ORAEXAMP.SQL.
OraDatabase.ExecuteSQL ("Begin Employee.GetEmpName (:EMPNO, :ENAME); end;")
'Display the employee number and name.
'Execute the Stored Function Employee.GetSal to retrieve SAL.
' This Stored Function can be found in the file ORAEXAMP.SQL.
OraDatabase.ExecuteSQL ("declare SAL number(7,2); Begin" & _
":SAL:=Employee.GetEmpSal (:EMPNO); end;")
'Display the employee name, number and salary.
MsgBox "Employee " & OraDatabase.Parameters("ENAME").value & ", #" & _
OraDatabase.Parameters("EMPNO").value & ",Salary=" & _
OraDatabase.Parameters("SAL").value
'Remove the Parameters.
'OraDatabase.Parameters.Remove "EMPNO"
'OraDatabase.Parameters.Remove "ENAME"
'OraDatabase.Parameters.Remove "SAL"
OraDatabase.ClearParameters
End Sub
そのまま書き換えた場合に比べ、大幅に単純化されていることが分かります。

補足

  • データプロバイダに OraOLEDB でなく MSDAORA を指定しても動きます。
  • OraAdapter.ParametersADODB.Parameter のコレクションです。
    したがって、OraDatabase.Parameters("EMPNO").serverType = ORATYPE_NUMBER は、
    OraDatabase.SetParameterServerType "EMPNO", ORATYPE_NUMBER
    ではなく
    OraDatabase.Parameters("EMPNO").Type = adNumeric
    に書き換えることも可能です。
  • CreateOraDynasetOraDynaset ではなく、ADODB.Recordset を返します。
    OraDynasetRecordset の違いについては、以下の記事も参考にしてください。
  • パラメータを変更してOraDynaset.Refresh をしている場合は、
OraAdapter.RefreshParameters
OraDynaset.Requery
に書き換えてください。

2018-01-13

VB.NETとC#からのExcel出力

検索すればあちこちで見つかる VB.Net および C# からの Excel 出力ですが、意外と以下の条件を満たすものがないようですので、サンプルコードを掲載しておきます。

条件

  1. Excel 終了時にきちんと COM の開放が行われ、EXCEL.EXE が終了する。
  2. 高速化(2次元配列を使用)している。
  3. 書式設定が行われている。
  4. 遅延バインディング

参考

Excelファイルを C# と VB.NET で読み込む “正しい” 方法 - Qiita

はじめに “Excel C#” や “Excel VB.NET” でググった新人プログラマが、古い情報や間違った情報で茨の道を選ばずに済むようにと思って書きました。 この記事は、Windows で Visual Studio を使用したデスクトップアプリケーション開発を想定しています。 VB.NET でも作成可能ですが、サンプルコードでは C# 6.0 を使用しています。どちらでもいいなら C# を使いましょう。 C# または VB.NET でExcel…

サンプルコードは、Microsoft.Office.Interop.Excel を使用しています。
(リンク先では推奨されていないのですが、外部DLLを使わずにすみますので)

サンプルコード

仕様

  • DataTable の内容を新規 Book に出力し、そのまま(保存せずに)表示します。
  • String 型は文字列にしています。DateTime型は、”yyyy/mm/dd”の形式にしています。(Date 型と DateTime 型で書式を変えたい場合は文字列化した方が良いと思います。)
  • 罫線と列幅の自動調整まで行っています。
VB.NET
Option Strict Off
Imports Microsoft.VisualBasic
Imports System.Data
Public Sub ExportExcel(ByVal dt As DataTable)
Dim xlApp As Object = Nothing
Dim xlBooks As Object = Nothing
Dim xlBook As Object = Nothing
Dim xlSheet As Object = Nothing
Dim xlCells As Object = Nothing
Dim xlRange As Object = Nothing
Dim xlCellStart As Object = Nothing
Dim xlCellEnd As Object = Nothing
Try
xlApp = CreateObject("Excel.Application")
xlBooks = xlApp.Workbooks
xlBook = xlApp.Workbooks.Add
xlSheet = xlBook.WorkSheets(1)
xlCells = xlSheet.Cells
Dim dc As DataColumn
Dim columnData(dt.Rows.Count, 1) As Object
Dim row As Integer = 1
Dim col As Integer = 1
For col = 1 To dt.Columns.Count
row = 1
dc = dt.Columns(col - 1)
'ヘッダー行の出力
xlCells(row, col).value = dc.ColumnName
row = row + 1
' 列データを配列に格納
For i As Integer = 0 To dt.Rows.Count - 1
columnData(i, 0) = String.Format(dt.Rows(i)(col - 1))
Next
xlCellStart = xlCells(row, col)
xlCellEnd = xlCells(row + dt.Rows.Count - 1, col)
xlRange = xlSheet.Range(xlCellStart, xlCellEnd)
' Excel書式設定
Select Case Type.GetTypeCode(dc.DataType)
Case TypeCode.String
xlRange.NumberFormatLocal = "@"
Case TypeCode.DateTime
xlRange.NumberFormatLocal = "yyyy/mm/dd"
'Case TypeCode.Decimal
' xlRange.NumberFormatLocal = "#,###"
End Select
xlRange.value = columnData
Next
xlCells.EntireColumn.AutoFit()
xlRange = xlSheet.UsedRange
xlRange.Borders.LineStyle = 1 'xlContinuous
xlApp.Visible = True
Catch
xlApp.DisplayAlerts = False
xlApp.Quit()
Throw
Finally
If xlCellStart IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(xlCellStart)
If xlCellEnd IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(xlCellEnd)
If xlRange IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(xlRange)
If xlCells IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(xlCells)
If xlSheet IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(xlSheet)
If xlBooks IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(xlBooks)
If xlBook IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(xlBook)
If xlApp IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(xlApp)
GC.Collect()
End Try
End Sub
C# (4.0以上)
using System.Data;
public void ExportExcel(DataTable dt)
{
dynamic xlApp = null;
dynamic xlBooks = null;
dynamic xlBook = null;
dynamic xlSheet = null;
dynamic xlCells = null;
dynamic xlRange = null;
dynamic xlCellStart = null;
dynamic xlCellEnd = null;
try
{
xlApp = Activator.CreateInstance(Type.GetTypeFromProgID("Excel.Application"));
xlBooks = xlApp.Workbooks;
xlBook = xlBooks.Add;
xlSheet = xlBook.WorkSheets(1);
xlCells = xlSheet.Cells;
DataColumn dc;
object[,] columnData = new object[dt.Rows.Count, 1];
int row = 1;
int col = 1;
for (col = 1; (col <= dt.Columns.Count); col++)
{
row = 1;
dc = dt.Columns[(col - 1)];
// ヘッダー行の出力
xlCells[row, col].value2 = dc.ColumnName;
row++;
// 列データを配列に格納
for (int i = 0; (i <= (dt.Rows.Count - 1)); i++)
{
columnData[i, 0] = string.Format("{0}",dt.Rows[i][(col - 1)]);
}
xlCellStart = xlCells[row, col];
xlCellEnd = xlCells[(row + (dt.Rows.Count - 1)), col];
xlRange = xlSheet.Range(xlCellStart, xlCellEnd);
// Excel書式設定
switch (Type.GetTypeCode(dc.DataType))
{
case TypeCode.String:
xlRange.NumberFormatLocal = "@";
break;
case TypeCode.DateTime:
xlRange.NumberFormatLocal = "yyyy/mm/dd";
break;
//case TypeCode.Decimal:
// xlRange.NumberFormatLocal = "#,###";
// break;
}
xlRange.value2 = columnData;
}
xlCells.EntireColumn.AutoFit();
xlRange = xlSheet.UsedRange;
xlRange.Borders.LineStyle = 1; // xlContinuous
xlApp.Visible = true;
}
catch
{
xlApp.DisplayAlerts = false;
xlApp.Quit();
throw;
}
finally
{
if (xlCellStart != null) System.Runtime.InteropServices.Marshal.ReleaseComObject(xlCellStart);
if (xlCellEnd != null) System.Runtime.InteropServices.Marshal.ReleaseComObject(xlCellEnd);
if (xlRange != null) System.Runtime.InteropServices.Marshal.ReleaseComObject(xlRange);
if (xlCells != null) System.Runtime.InteropServices.Marshal.ReleaseComObject(xlCells);
if (xlSheet != null) System.Runtime.InteropServices.Marshal.ReleaseComObject(xlSheet);
if (xlBook != null) System.Runtime.InteropServices.Marshal.ReleaseComObject(xlBook);
if (xlBooks != null) System.Runtime.InteropServices.Marshal.ReleaseComObject(xlBooks);
if (xlApp != null) System.Runtime.InteropServices.Marshal.ReleaseComObject(xlApp);
GC.Collect();
}
}

C# での注意点

  • 遅延バインディングにするため dynamic 型を使用しているので 4.0 以上が必要です。
  • Microsoft.Csharp の参照設定をしてください。

ネット上のコードで気になったこと

Workbooks の変数格納

Workbooks オブジェクトを変数に格納せず xlSheet = xlBooks.Add.WorkSheets(1); としているものも見受けられました。
c# - How do I properly clean up Excel interop objects? - Stack Overflow
試したところ、VB.NET では変数に格納しなくても EXCEL.EXE は終了しましたが、C# では終了しませんでした。サンプルコードでは VB.NET でも変数に格納するようにしています。

get_Range メソッドとRange プロパティ

Range の 取得に get_Range メソッドを使っている例がありましたが、 get_Range メソッドは MSDN

セルまたはセルの範囲を表す Microsoft.Office.Interop.Excel.Range オブジェクトを取得します。 このメソッドの代わりに Range プロパティを使用してください。

との記載があるのと、c# - Worksheet get_Range throws exception - Stack Overflow
によると、.NET4.0 からエラーになるようです。サンプルコードでは、Range プロパティを使用するようにしました。

Range.ValueRange.Value2

Excel のセルへの書き込みにValue プロパティと Value2 プロパティを使っているものがあります。両者の違いは

Value プロパティとの相違点は、Value2 プロパティでは、通貨型 (Currency) および日付型 (Date) のデータ型を使用しない点だけです。

のようです。
なぜ、二つのコードが出回っているかと言うと、VB.NET ではインテリセンスで Value が出て、C# では Value2 が出るため(Value プロパティにはパラメータがあるが、C# はパラメータ付きプロパティをサポートしていないため)で実質的な違いはないようです。
Parameterized Properties in C# and the mystery of Value2 in Excel – .NET4Office

ReleaseComObjectFinalReleaseComObject

COM の開放に FinalReleaseComObject を使っているものもありましたが、
How to properly release Excel COM objects: C# code examples によると、 FinalReleaseComObject を使うのは冗長のようですので、サンプルコードではReleaseComObject を使っています。

2018-01-09

Why "Don't track my views for this blog." does not work in Blogger

Don't track my views for this blog.
In the Blogger dashboard, when you click Stats -> Overview ->Manage tracking your own pageviews and check Don't track my views for this blog., your pageviews are expected to be excluded from the stats.
But your pageviews might be counted, and the checkbox is unchecked each time you restart the browser.

Causes

When Don't track my views for this blog. is checked, it invokes the following JavaScript setting the Cookie.

var COOKIE_NAME = '_ns';
var COOKIE_SET_VALUE = '2';
document.cookie = COOKIE_NAME + '=' + COOKIE_SET_VALUE;

But it has some issues as follows.

Domain attribute

It doesn’t specify the domain attribute, so current sub domain, blogname.blogspot.com, is set to the Cookie. If you are redirected to a country-specific URL (ccTLD), blogname.blogspot.ccTLD, the cookie is not sent because it has the different domain.(Redirect to the ccTLD has been expired. See Official Blogger Blog: It’s spring cleaning time for Blogger.)

Note: The domain attribute is supposed to be specified to apply the cookie to all sub domains under the domain. If you want the cookie for the specific sub domain like this case, you need not specify the domain attribute. And you can not specify the other domain you are requesting.

Path attribute

It doesn’t specify the path attribute, so the current page path /b is set for the path attribute. As a result, the cookie is sent only when requested pages are under /b/.

Expires attribute

As the expires attribute is also not specified, the cookie is deleted when the browser is closed (aka Session Cookie).

FYI

Workaround

The script modified the above issues is as follows.

document.cookie = "_ns=2; expires=Tue, 19 Jan 2038 03:14:07 GMT; path=/";

If you are using Chrome, after visiting your blog, start Developer Tools (press F12). Paste this script in Console and run (press Enter).

Chrome Developer Tool
(In Chrome Developer Tool, you can see and edit the Cookies at Application -> Cookies.)

  • You can’t set a cookie to never expire. So I set 03:14:07 UTC on Tuesday, 19 January 2038 for the expires attribute, the latest time avoiding the Year 2038 problem.
    I don’t use max-age attribute because IE11 doesn’t support.

If your access to your blog is redirected to blogname.blogspot.ccTLD and you want to keep Don't track my views for this blog. unchecked (which has no practical sense), execute the above script after visiting blogger’s dashboard as well.


Here is another version, which expires date is in a plain way.

var d = new Date("2038-01-19"); document.cookie = "_ns=2; expires=" + d.toGMTString() + "; path=/";

You can modify "2038-01-19" as you like (not to exceed “2038-01-19”).
If you specify the date in the past, you can delete the cookie.

Setup with Smartphones

You may want not to count the pageviews from smartphones, which browser doesn’t have Developer Tools. In such case, after visiting your blog with your smartphone’s browser, clear the address bar and type javascript: and paste the above script just after it, then press Enter.

And, if you input javascript:document.cookie; in the address bar (the last semicolon is probably not necessary), the current page’s cookie can be shown. If _ns=2 is displayed, the cookie is set properly.