.NET’te COM ile Birlikte Çalışma (.NET & COM Interoperability)

.NET teknolojisi, COM nesnelerini yok saymayıp aksine onlarla bütünleşik bir çalışma ortamı sunar. Hem COM nesnelerinin .NET içinde kullanılmasına hem de .NET nesnelerinin COM içinde kullanılmasına destek verir. Birçok şeyi .NET nesneleriyle daha kolay ve hızlı yapabiliz ancak yeni projelerde bazı eski COM nesnelerimizi kullanmak zorunda kalabiliriz. Bu durumda nasıl bir etkileşim sözkonusu olacak. Bu yazıda .NET bileşenlerinin var olan COM bileşenlerini değiştirmeden onlarla nasıl iletişim kuracaklarını işleyeceğiz.
COM mimarisinde bileşenler arası haberleşmeler, IUnknown ve IDispatch gibi arabirimler aracılığıyla gerçekleşmekteydi. Fakat .NET’te bileşenler arası haberleşmeler, Object’ler aracılığıyla gerçekleştirilir. .NET’te type libraryler bulunmayıp onun yerine assemblyler kullanılır.

Type library, COM bileşenine ait arabirimleri ve veri tiplerini açıklayan metaveriyi içeren derlenmiş dosyadır(.tlb). COM componentini MTS’e yüklemek için gerekli olan bilgileri sağlar. Bilindiği gibi Assembly, işlevsel olarak birbirleriyle ilişkili tür ve kaynakların oluşturduğu mantıksal bir birimdir(.dll/.exe). Assembly ile ilgili tüm bilgiler, assemblynin metadatasında saklanır.
COM Nesnelerini .NET’ten Çağırmak

Mimarilerinin farklı oluşundan dolayı bir .NET bileşeni, COM bileşenleriyle doğrudan haberleşemez. Çünkü COM’un iletişim için sunmuş olduğu Interfaceler, .NET uygulaması tarafından okunamaz. Bu yüzden, COM bileşeni, çalışma zamanında .NET uygulamasının anlayabileceği şekilde özel bir bileşenle kaplanır. Bu bileşene RCW(< ı>Runtime callable wrapper-Çalışmazamanında çağrılabilen sarıcı) denilir. RCW, çalışma zamanında COM bileşenini sarar ve CLR ile arasına girer. .NET istemci ile COM bileşeni arasında bir bağ kurulur böylece COM bileşeni, .NET uygulamasına normal bir .NET bileşeniymiş gibi görünür. Burada COM bileşenlerinin sunucuya register edilmiş olması gerekmektedir.

NET ortamında bir client, COM bileşenine erişmek istediği zaman RCW’i iki şekilde oluşturur.

    Visual Studio’daki projenin References bölümünden “Add Reference” menüsünü kullanmak. Bu menü hatırlarsanız, hem .NET hem de COM nesnelerine erişmemizi sağlayan dialog penceresini açıyordu. Bu pencerede COM sekmesinin altında, sunucunda kayıtlı COM bileşenlerine erişebiliriz. Bunlardan birini seçip proje eklediğimiz VS otomatik olarak RCW oluşturacaktır. İlgili bileşenin önüne Interop ifadesini ekler.
    Veya alternatif olarak VS ile birlikte gelen TlbImp.exe(Type Library Importer) adlı komut satırı aracı kullanılabilir. Bu komut daha çok VS’in olmadığı durumlarda tercih edilir.TlbImp aracı, COM nesnelerinin .NET uygulaması tarafında referans olarak kullanılabileceği şekilde sarmalar.

Her iki yöntem de temel olarak System.Runtime.InteropServices.TypeLibConverter sınıfını kullanır.
Bu iletişim sürecini hafıza yönetimi açısından değerlendirecek olursak, RCW, COM bileşenini Garbage Collector çalışıp çöpler toplanıncaya serbest bırakmaz. Garbage Collector RCW içindeki Finalize metodunu çağırır. RCW de içerden COM nesnesi içinde IUknown interfacenin Release metodunu çağırır. Fakat COM nesnesini memoryden temizlemek için GC’yi beklemek istenilmeyen durumlar oluşturabilir. Bu durumda COM nesnesiyle işimiz bittikten sonra GC’yi manual çalıştırabiliriz. Ancak GC’nin o andaki yükünün yoğun olması diğer işlemleri yavaşlatabilir. GC’nin o anda sorumlu tüm nesneler için çalışmasını değil de sadece bu COM nesnesini yok etmek istersek < ı>System.Runtime.InteropServices.Marshal.ReleaseComObject fonksiyonu kullanılabilir.
Şimdi yazdıklarımızı bir örnek üzerinde uygulayalım. Kendimiz VB 6.0 gibi bir ortamda unmanaged bir component yazıp .NET ortamında kullanabiliriz. Ancak burada yeni bir component yazmak yerine Office ile birlikte gelen Excel uygulamasına ait EXCEL.EXE içindeki bileşeni örnek olarak tercih edeceğiz. Öncelikle yeni bir Windows projesi oluşturalım. Daha sonra projenin references bölümünde Add Reference pencesini açtıralım. Buradaki COM sekmesinde Microsoft Excel 11.0 nesnesini ekleyelim. Bu nesne, benim makinemde “< ı>c:\Program Files\Microsoft Office\OFFICE11\excel.exe” pathinde bulunmaktadır.

Bu bileşeni referansları eklediğimiz Visual Studio, otomatik olarak ilgili RCW componentlerini oluşturur.

Projede Form’un üzerine bir Button ekleyelim. Button’a tıklandığı zaman program içinde şekillenmiş bir Excel dosyası oluştursun. Bu componentte hiyerarşik olarak şu şekilde bir yapı bulunur.
Application: Bileşenin en üstünde bulunur. Workbooklara erişir.
Workbook: Excel dosyasının oluştuğu spreadsheet grubudur.
Worksheet: Bir excel spreadsheet’idir.
Range: Bir ve daha fazla hücreyi temsil eder.

Imports System
Imports System.Windows.Forms
Imports System.Reflection
Imports System.Runtime.InteropServices

Public Class Form1

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        'Excel nesnesi oluşturalım.
        Dim ExcelApp As Excel.ApplicationClass = New Excel.ApplicationClass()
        'Excel'e bir Worksheet ekleyelim.
        ExcelApp.Workbooks.Add(True)

        'Hücrelere gerekli başlıkları ve değerleri atalım.
        ExcelApp.Range("A1", Missing.Value).Value = "Sayı"
        ExcelApp.Range("B1", Missing.Value).Value = "Karesi"
        ExcelApp.Range("A1", Missing.Value).Font.Bold = True
        ExcelApp.Range("B1", Missing.Value).Font.Bold = True

        For i As Integer = 2 To 7
            ExcelApp.Range("A" & i, "A" & i).Value = i
            ExcelApp.Range("B" & i, "B" & i).Value = i * i
        Next i
        'Excel dosyasını kullanıcıya gösterelim.
        ExcelApp.Visible = True
        'COM nesnesini hafızadan kaldıralım.
        Marshal.ReleaseComObject(ExcelApp)

    End Sub
End Class

using System;
using System.Windows.Forms;
using System.Reflection;
using System.Runtime.InteropServices;
namespace WindowsApplication1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
//Excel nesnesi oluşturalım.
Excel.ApplicationClass ExcelApp = new Excel.ApplicationClass();
//Excel'e bir Worksheet ekleyelim.
ExcelApp.Workbooks.Add(true);
//Hücrelere gerekli başlıkları ve değerleri atalım.
ExcelApp.get_Range("A1", Missing.Value).set_Value(Missing.Value, "Sayı");
ExcelApp.get_Range("B1", Missing.Value).set_Value(Missing.Value, "Karesi");
ExcelApp.get_Range("A1", Missing.Value).Font.Bold = true;
ExcelApp.get_Range("B1", Missing.Value).Font.Bold = true;
for (int i = 2; i < 8;="">
{
ExcelApp.get_Range("A"+i, "A"+i).set_Value(Missing.Value, i);
ExcelApp.get_Range("B" + i, "B" + i).set_Value(Missing.Value, i*i);
}
//Excel dosyasını kullanıcıya gösterelim.
ExcelApp.Visible = true;
//COM nesnesini hafızadan kaldıralım.
Marshal.ReleaseComObject(ExcelApp);
}//button1_Click
}
}

Bu programı çalıştırıp butonu tıkladığımızda içinde aşağıdaki değerlerin olduğu bir excel dosyası açılır.

Eğer bu uygulamada daha ilk satırda aşağıdaki gibi hata alırsanız bu, sizden değil Office’den kaynaklanıyor. Excel’in İngilizce versiyonunda ortaya çıkan bu bug, bilgisayarınızın dilinin İngilizce olmamasından kaynaklanmaktadır.

Bunu aşmak için Excel nesnesi oluşturmadan önce uygulamanın Culture değerini İngilizce’ye göre ayarlayabilirsiniz. Sayfa sonunda Culture bilgilerini tekrar geri alırsınız. Veya Control Panel’den Regional Settings and Languages bölümünden English(US)’i seçebilirsiniz.

Dim thisThread As System.Threading.Thread = _
            System.Threading.Thread.CurrentThread
Dim originalCulture As System.Globalization.CultureInfo = _
    thisThread.CurrentCulture
        thisThread.CurrentCulture = New System.Globalization.CultureInfo("en-US")
'::::::::Excel ile ilgili kodlarımız:::::::::::::

'Culture değerlerimizi geri alalım.
thisThread.CurrentCulture = originalCulture
System.Threading.Thread thisThread =
System.Threading.Thread.CurrentThread;
System.Globalization.CultureInfo originalCulture =
    thisThread.CurrentCulture;
thisThread.CurrentCulture = new System.Globalization.CultureInfo(
"en-US");
//::::::::::Excel ile ilgili kodlarımız::::::::::::::

//Culture değerlerimizi geri alalım.
thisThread.CurrentCulture = originalCulture;

Visual Studio’nın tercih edilmediği durumlarda TlbImp aracı kullanılabilir. Command Prompt’dan; TlbImp şeklindeki komuttan sonra COM bileşeninin RCW bileşenleri oluşur. O bileşenleri de .NET projelerine referans ekleyerek kullanabiliriz.
.NET Nesnelerini COM’dan Çağırmak

Yönetilemeyen COM nesnelerini, .NET platformunda nasıl kullanılacağını öğrenmiş olduk. .NET, bu işlemin tersine de destek verir. Yani programcılıkta nadir de olsa .NET nesneleri COM istemcileri tarafından kullanılabilir.
Bir COM istemci, bir sunucuya istek göndereceği zaman öncelikle onu registry kayıtlarında arar ve iletişimi öyle başlatır. Önceki sayfada .NET bileşenlerinin, Object’ler aracılığıyla iletişim kurduğunu söylemiştik. Object tabanlı bir etkileşim, COM istemcileri tarafından tanınmadığı için, COM bileşeninin içinden .NET bileşenine erişmek için .NET bileşenini COM istemcisinin tanıyabileceği şekle dönüştürmek gerekir(Wrapping). Bu dönüştürücüye CCW(< ı>COM callable wrapper-COM cağrılabilen sarıcı). CCW, RCW gibi bir tür olup, .NET assemblylerinin COM istemcileri tarafından referans verilmesini sağlar.

CCW, Framework ile birlikte gelen RegAsm.exe(Register Assembly) veya TlbExp.exe(Type Library Exporter) araçlarıyla oluşturulur. Bu araçlar, .NET bileşeninin metadatasını okur ve CCW türünü oluşturur. Ayrıca bileşen ile ilgili registry üzerinde kayıt açar.
Bilindiği gibi COM istemcileri genellikle coCreateInstance metodunu kullanarak bileşenden nesne yaratır. .NET componentinin COM içinde kullanıldığı senaryoda coCreateInstance metodu çağrıldığı zaman çağrı, registry kaydına yönlendirilir, registry de çağrıyı sunucu üzerinde register edilmiş mscoree.dll kütüphanesine yönlendirir. mscoree.dll, kendisine gönderilen CLSID’ye göre registry üzerinde arama yaparak .NET assemblysinin yerini öğrenir. Ve iletişim bu şekilde başlar. İstemci, .NET nesnesinden bir istekte bulunduğu zaman istek öncelikle CCW’ye gider. CCW, tüm COM türlerini .NET tarafındaki eşdeğerlerine dönüştürür ve .NET’ten geri dönen sonuçları COM’un anlayabileceği formata dönüştürür.
Bir .NET assemblysi COM istemcileri tarafından erişilecekse sırayla şu adımlar uygulanır;
1. Bir Strong Name verilmesi
2. Assemblynin GAC’a yerleştirilmesi.
3. Assemblynin bir COM nesnesiymiş gibi register edilmesi.
4. COM nesnesinin çağrılması.
Bilindiği gibi eğer assembly sistem klasöründe veya uygulamayla aynı konumdaysa GAC’a atılması bir gereklilik değildir. Ve Strong Name, sadece assembly, GAC’a kurulacağı zaman gereklidir. Ancak .NET bileşeni COM tarafından kullanılma durumunda yukarıdaki aşamaların uygulanması tavsiye edilir.
COM istemcisi, .NET bileşenin içindeki tüm üyelere erişemez. Sadece public üyelere erişebilir. Ayrıca parametreli constructor, statik metotlara ve sabit alanlara erişemez.
Bir Class Library projesi oluşturup küçük bir örnek yapalım. Class1 dosyasını Matematik olarak aşağıdaki şekilde değiştirelim.

Imports System
Imports System.Collections.Generic
Imports System.Text

Public Class Matematik
    'Sayının faktoriyelini alır.
    Public Function Faktoryel(ByVal Sayi As Integer) As Integer
        Dim result As Integer = 1

        Dim i As Integer = 2
        Do While i 

Şimdi RegAsm komutunu uygularsak, "< ı>Types registered successfully" mesajını alırız. Sunucuya register ettiğimiz bu componenti herhangi bir COM istemcisinde kullanabiliriz. Burada bir VBScript dosyası oluşturacağız aynı örnek bir asp sayfasında da kullanılabilir. Ornek.vbs isminde bir dosya oluşturup aşağıdaki gibi düzenleyelim.
Dim objMath
Set objMath = CreateObject("ClassLibrary1.Matematik")

msgbox("5'in faktöriyeli :"& objMath.Faktoryel(5))

.vbs dosyasını çift tıklayıp çalıştırdığımız zaman ekrana 5’in faktöriyelini yazdırır. Eğer .vbs dosyasında Kare metoduna erişmek istersek Kare private olduğundan COM istemciler tarafından erişelemeyeceği için hata mesajı verilecektir.

Bu bileşen ile ilgili registry kaydına bakmak için registry’da “HKEY_LOCAL_MACHINE » Classes” (< ı>HKEY_CLASSESE_ROOT » Classes Win XP)bölümünden componentin CLSID’sini öğrenelim.

Buradan öğrendiğimiz CLSID’yi “HKEY_LOCAL_MACHINE » Classes » CLSID” klasörünün altında arayalım.

Şekilde InprocServer32 altında mscoree.dll dosyası görünmektedir. Bu da gösteriyor ki unmanaged istemcilerin muhatabı doğrudan bizim managed assembly değil assemblyi uyarlayacak olan CLR çalıştırma ortamıdır. InprocServer32’nin altındaki Class deyimi mscoree.dll dosyasına hangi sınıftan nesne oluşturacağını, Assembly değeri de bu sınıfın hangi assemblyde saklı olduğunu bildirir.
Bilindiği gibi COM bileşenlerinden iki şekilde nesne oluşturulur; Geç bağlama(late binding) ve erken bağlama(early binding). Erken bağlama klasik ASP’de kullandığımız CreateObject yöntemidir. Geç bağlama ise VB 6.0’da olduğu gibi New operatörünü kullanmaktır. Geç bağlama durumunda örneğimiz için şu şekilde bir ifade oluşturulur;

Dim Math As Matematik
'Veya
Set objMath = New ClassLibrary1.Matematik
objMath.Faktoryel

Geç bağlama yönteminde VB 6.0’da type library referanslara eklenip öyle oluşturuluyordu. Bu yüzden eğer .NET componenti early binding yöntemiyle kullanılacaksa öncelikle type library’nin oluşturulması gerekir. Bunun için de ya
TlbExp ClassLibrary1.dll
şeklinde bir TlbExp aracı kullanılır ya da RegAsm’nin tlb parametresi kullanılır;
RegAsm ClassLibrary1.dll /tlb: ClassLibrary1.tlb
.NET nesnesini registry kayıtlarından kaldırmak için de “U” parametresi kullanılır.
RegAsm ClassLibrary1.dll /U
.NET’de ActiveX Kullanımı

Klasik COM bileşenleri dışında görsel bileşen olan ActiveX’leri de .NET uygulamalarında kullanabiliriz. Bunun için de yine aynı şekilde Visual Studio kullanılabileceği gibi Aximp.exe (ActiveX Control Importer) aracı da kullanılır. Buna örnek olarak MS Web Browser’ı(shdocvw.dll) verebiliriz. Bu ActiveX’in bileşeni benim makinemde < ı>c:\windows\system32\shdocvw.dll altında bulunmaktadır. VS’da yeni bir windows projesi oluşturup Toolbox’ı sağ tıklayıp “Choose Items” menüsünden “Choose Toolbox Items” penceresini açtırıp COM Components sekmesini tıklayalım. Burada Microsoft Web Browser‘i seçip bileşenin Toolbox’a ekleyelim.

Bu nesneyi toolboxtan Form üzerine taşıyıp projeyi derlediğimizde ilgili RCW bileşenleri oluşur.

AxWebBrowser nesnesinin özelliklerini properties panelinden ayarlayabiliriz. Ben, browser, formun tümüne yerleşsin diye Dock özelliğini < ı>Fill olarak set ettim.

Me.AxWebBrowser1.Dock = System.Windows.Forms.DockStyle.Fill
this.axWebBrowser1.Dock = System.Windows.Forms.DockStyle.Fill;

Ve form açıldığı zaman Google adresini gösterecek şekilde ayarlayalım.

Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
    AxWebBrowser1.Navigate("http://www.google.com.tr")
End Sub
private void Form1_Load(object sender, EventArgs e){
    axWebBrowser1.Navigate("http://www.google.com.tr");
}

VS 2005 ile birlikte WebBrowser nesnesi geliyor olsa da eski COM bileşenini de bu şekilde kullanabiliriz. Programı derlediğimizde aşağıdaki görüntüyü elde ederiz.

Web Browser bileşeninin RCW’leri VS kullanılmadan AxImp komut aracılyla şu şekilde oluşturulabilir;
aximp c:\windows\system32\shdocvw.dll
Windows Kontrollerini IE içinde kullanmak

“.NET ortamında ActiveX oluşturmak” yerine bu başlığı seçmemin nedeni .NET platformunda gerçekten her yerde çalışabilecek bir ActiveX yapısının oluşturulamıyor olmasıdır. Windows formlarını, web browserlarda çalıştırabilmek için clientlarda .NET runtime’ın kurulu olması gerekiyor. Bu da şu aşamada çok zor olduğu için bu tür uygulamalar genellikle Intranet veya belli local alanlarda çalışma üzere geliştirilir.
Evet, CLR, sayfalarımızı daha işlevsel yapmak için herhangi bir Windows Form kontrolünü, herhangi bir HTML sayfasında kullanmamıza izin verir. Bunun için aşağıdaki adımlar takip edilir;

    Windows Form kontrolünün geliştirilir
    Kontrol, HTML sayfasına tagı içerisinde tanımlanır.
    Virtual Directory oluşturulur ve yetkileri ayarlanır.

HTML sayfasında Windows DateTimePicker sınıfından üretilmiş bir takvim bileşeni oluşturalım. Onu da HTML sayfalarında kullanalım. VS’de adı OrnekKontrol olan bir Class Library projesi (Windows Control Library de olabilir) oluşturarak örneğimizi oluşturalım. DateTimePicker nesnesini kullanacağımız için projeye System.Windows.Forms kütüphesini referans olarak eklemeliyiz. Class1 dosyasını aşağıdaki şekilde düzenleyelim.

Imports System
Imports System.Windows.Forms
Imports System.ComponentModel

Public Class Takvim
    Inherits System.Windows.Forms.DateTimePicker

    Public Sub New()
        MyBase.New()
        InitializeComponent()
    End Sub

    Private Sub InitializeComponent()
        'DateTimePicker nesnesinin Format'ını belirleyelim.
        Me.Format = DateTimePickerFormat.Short
    End Sub
End Class
using System;
using System.Windows.Forms;
using System.ComponentModel;

namespace OrnekKontrol{

    public class Takvim : System.Windows.Forms.DateTimePicker{

        public Takvim(): base(){
			InitializeComponent();
		}

        private void InitializeComponent(){
            //DateTimePicker nesnesinin Format'ını belirleyelim.
            this.Format = DateTimePickerFormat.Short;
        }

    }//Takvim
}

Projeyi derleyip .dll dosyasını wwwroot altında Bilesen isimli bir klasöre yerleştirelim. wwwroot altında Ornek.html dosyasını oluşturalım. Bu tür kontroller, HTML içinde tagı içinde şeklinde tanımlanır. ClassId aşağıdaki gibi tanımlanır.


Internet Explorer içinde client script dilleri kullanılarak bu kontrolün public üyelerine erişilebilir. Bunun için class için ComVisible niteliğini < ı>true yapmamız gerekir. Örneği biraz daha geliştirip IE içinden JavaScript aracılığıyla bu nesnenin bazı bilgilerine erişelim. Kodumuza < ı>Tarih isimli bir property ve < ı>FormatDegistir isimli bir metod ekleyelim.

Imports System
Imports System.Windows.Forms
Imports System.ComponentModel

Public Class Takvim
    Inherits System.Windows.Forms.DateTimePicker

    Public Sub New()
        MyBase.New()
        InitializeComponent()
    End Sub

    Private Sub InitializeComponent()
        'DateTimePicker nesnesinin Format'ını belirleyelim.
        Me.Format = DateTimePickerFormat.Short
    End Sub

    'Tarih property
    Public ReadOnly Property Tarih() As String
        Get
            Return Value.ToShortDateString
        End Get
    End Property

    'Formatı Long veya Short olarak değiştirir.
    Public Sub FormatDegistir()
        If Me.Format = DateTimePickerFormat.Long Then
            Me.Format = DateTimePickerFormat.Short
        Else
            Me.Format = DateTimePickerFormat.Long
        End If
    End Sub

End Class
using System;
using System.ComponentModel;
using System.Windows.Forms;

namespace OrnekKontrol
{
    public class Takvim : System.Windows.Forms.DateTimePicker{

        public Takvim(): base(){
			InitializeComponent();
		}

        private void InitializeComponent(){
            this.Format =DateTimePickerFormat.Short;
        }

        //Tarih property
        public string Tarih{
            get { return Value.ToShortDateString(); }
        }

        //Formatı Long veya Short olarak değiştirir.
        public void FormatDegistir() {
            if (this.Format == DateTimePickerFormat.Long)
                this.Format = DateTimePickerFormat.Short;
            else
                this.Format = DateTimePickerFormat.Long;
        }

    }//Takvim
}

Html tarafını da aşağıdaki gibi düzenleyelim.


Şekilde görüldüğü gibi “Tarih Oku” butonu tıklandığı zaman Takvim nesnesinin Tarih propertysinden değer okunur. “Tarih Formatı Değiştir” tıklandığında da nesnenin FormatDegistir metodu tetiklenmiş olur.
Windows Form kontrolünü kullandığımız HTML sayfalarının IIS üzerinde bir virtual directory içinde olması gerekir. Virtual directorydeki Execute Permissions yetkisi scripts olarak set edilmelidir. scripts & executables olarak set edilirse kontrol yüklenmez.
.dll dosyası uygulamayla aynı klasörde olabildiği gibi GAC’a yüklenebilir. Ayrıca .dll üzerinde yaptığınız bir değişikliğin html tarafına yansıması için browseri kapatıp yeniden açmak gerekir.

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Time limit is exhausted. Please reload CAPTCHA.