فصل ۲۱ — Class Moduleها
هنگام برنامهنویسی VBA و افزودن Moduleهای معمولی، در منوی Insert گزینهٔ Class Module را نیز میبینید. Class Module با Module عادی متفاوت است؛ زیرا اجازه میدهد نوعی Component Object Model یا COM مخصوص خودتان بسازید.
Class Module مانند Procedure در Module استاندارد مستقیماً Run نمیشود و باید از Code دیگری به آن Reference شود. با آن میتوانید Objectها و Collectionهای اختصاصی خود را، مشابه TableDef و Collectionِ TableDefs، طراحی کنید. در Access VBA نمیتوان مانند Visual Basic کامل یا C++/C# مستقیماً DLL ساخت، اما Class Module همچنان بخشی از Database است و Database دیگری میتواند به Database شامل Classها Reference بدهد. به این ترتیب Object Component و نوعی معماری Multitier ایجاد میشود.
Access خود یک Application چندلایه است: Client Services Layer، مدلهای شیء زیر آن و Data Services Layer. Class Moduleها اجازه میدهند Layer دیگری بین Client Services و Access Object Model، یا بین Client Services و External Data Sourceهایی مانند Oracle یا SQL Server، قرار دهید.
Database حاوی Class Module میتواند از Database دیگری Reference شود. Public Functionها و Subroutineهای آن از بیرون قابل استفادهاند و اگر Project را Password-Protect کنید، Developer استفادهکننده Object را میبیند و استفاده میکند ولی Underlying Code و Ruleها را نمیبیند.
برای درک مفهوم، TableDef یک Object است که Hierarchy مربوط به هر Table را در خود دارد. Collectionِ Fields همهٔ Fieldهای Table را تعریف میکند و Count تعداد آنها را میدهد:
MsgBox CurrentDb.TableDefs("USyRribbons").Fields.Count
در مثال کتاب Tableِ USysRibbons دو Field دارد. Count فقط Read-only است؛ نمیتوان با تغییر عدد Count مستقیماً Field جدید ساخت، زیرا این کار Integrity Table را به هم میزند. طراحان Access Ruleهای Object Model را تعیین کردهاند و شما نیز در Classهای خود میتوانید مشخص کنید چه Property/Methodهایی وجود داشته باشند و کدام مقدار Read-only یا قابلتغییر باشد.
مثال فصل Objectهایی از Nameها میسازد. Collection با نام PNames مجموعهای از Objectهای PName خواهد بود و Nameها میتوانند نام Person یا Place باشند.
ساخت Data Services Layer
ابتدا Access Database جداگانهای با نام MyData برای Data Services Layer بسازید. Table جدیدی ایجاد کنید، Field متنی MyName را اضافه و Table را با نام MyTable ذخیره کنید. چند Record نمونه وارد کنید. همین Data Layer میتوانست SQL Server یا Oracle باشد، ولی مثال از Access استفاده میکند.
Data Services Layer باید از Object Model جدا باشد؛ ترکیب آنها میتواند Locking Problem ایجاد کند.
درج Class Module
از VBE گزینهٔ Insert | Class Module را انتخاب کنید. Class پیشفرض با نام Class1 ایجاد میشود؛ فوراً نام آن را تغییر دهید، زیرا همین Name در Code به Object تبدیل میشود و نامی مانند Class1 معنی Functionality آن را نشان نمیدهد.
Property Window را با F4 باز و Name را به MyPName تغییر دهید. سپس Instancing را روی PublicNotCreatable قرار دهید تا Object برای Applicationهای دیگر Visible باشد.
ساخت Object
ابتدا Object منفردی را میسازیم که بعداً عضو Collection خواهد شد. MyPName یک Property به نام PName دارد که Name واقعی را نگه میدارد. در Declarations Section:
Private mPname As String
Variable خصوصی mPname Working Variable داخل Class است و از بیرون Object دیده نمیشود. Name آن با Public Property متفاوت انتخاب شده و Prefixِ m نشاندهندهٔ Member Variable است.
برای Read/Write Property، یک Property Let و یک Property Get Public لازم است:
Public Property Let Pname(vdata As String)
mPname = vdata
End Property
Public Property Get Pname() As String
Pname = mPname
End Property
vdata Value واردشده به Property است. Let داده را داخل mPname مینویسد و Get مقدار mPname را برمیگرداند. نام Let و Get باید یکسان باشد؛ در غیر این صورت ممکن است Property فقط خواندنی یا فقط نوشتنی شود. از Insert | Procedure نیز میتوان Skeleton خودکار Property را ساخت تا Nameها سازگار باشند.
شکل ۲۱-۱ — استفاده از Add Procedure برای ساخت Property
اکنون Object منفرد PName آماده است. مرحلهٔ بعد ساخت Collectionِ MyPNames است، مشابه رابطهٔ Worksheet با Worksheets.
ساخت Collection
Class Module دیگری بسازید، Name آن را MyPNames و Instancing را PublicNotCreatable کنید. Variable خصوصی Collection:
Private mPnames As Collection
سپس Event/Procedureِ Class_Initialize را ایجاد کنید. این Routine نخستین بار که Class استفاده میشود اجرا و Content Collection را آماده میکند:
Private Sub Class_Initialize()
Dim objPname As New MyPName
Dim MyConn As New ADODB.Connection, ReSet As New ADODB.Recordset
Dim ConStr As String
ConStr = "Driver={Microsoft Access Driver (*.mdb, *.accdb)};" & _
"Dbq=MyTable.accdb;" & _
"DefaultDir=C:\MyPath\;" & _
"Uid=Admin;Pwd=;"
MyConn.ConnectionString = ConStr
MyConn.Open
Set ReSet = MyConn.Execute("select * from MyTable")
Set mPnames = New Collection
Do Until ReSet.EOF
Set objPname = New MyPName
objPname.Pname = ReSet!MyName
ReSet.MoveNext
mPnames.Add objPname
Loop
End Sub
objPname از Typeِ MyPName است. چون Classها ساخته شدهاند، MyPName و MyPNames هنگام تایپ Code در List Box ظاهر میشوند. برای ارتباط با Data Services Layer از ADO Connection و Recordset استفاده میشود؛ کتاب ADO را در اینجا راهی مؤثر برای Connection خارجی میداند، چون Linked Table/DSN نیازمند Setup روی Computer اجراکننده است.
ConStr Driver، Database Name و Location را مشخص میکند و در صورت نیاز میتواند User ID/Password نیز داشته باشد. سپس mPnames به Collection خالی جدید تبدیل میشود.
Loop دادهها را از MyTable میخواند. در هر Iteration یک Instance جدید MyPName ساخته، Pname از Fieldِ MyName مقداردهی و Object به Collection اضافه میشود. در پایان تمام Recordها به Collection تبدیل شدهاند.
برای دسترسی به عضو بر اساس Index، Function عمومی Item اضافه کنید:
Public Function Item(Index As Integer) As Pname
Set Item = mPnames.Item(Index)
End Function
این Function Wrapper یا «Mirror» روی Item داخلی mPnames است.
Property فقطخواندنی Count:
Public Property Get Count() As Long
Count = mPnames.Count
End Property
فقط Get لازم است، چون Count باید توسط Initialize و Collection زیربنایی تعیین شود و نباید مستقیماً تغییر کند.
در نتیجه Collectionِ MyPNames از Objectهای MyPName ساخته شده است و Count و Item را در Interface عمومی ارائه میکند. متن کتاب در یک جملهٔ بعدی Source را USysRibbons مینامد، هرچند Code مثال از MyTable استفاده میکند؛ خروجی حاضر این تفاوت متن اصلی را اصلاح نکرده و هر دو Reference را همانطور که در منبع آمده حفظ میکند.
استفاده از Collectionِ PNames
اکنون MyPNames را در Module استاندارد مانند Objectهای Built-in Access استفاده کنید:
Sub test_class()
Dim pn As New MyPNames, n As Integer
MsgBox pn.Count
For n = 1 To pn.Count
MsgBox pn.Item(n).Pname
Next n
End Sub
هنگام ساخت Instanceِ pn، Class Initialize اجرا، Source Data خوانده و Objectها به Collection اضافه میشوند. n Integer است چون Parameterِ Index در Item نیز Integer تعریف شده و برای For..Next مناسب است؛ Type ناسازگار میتواند Type Mismatch بدهد.
Loop بر اساس Count همهٔ Objectها را طی میکند. Functionِ Item و Propertyِ Count مانند Objectهای داخلی در Auto-completion ظاهر میشوند و Propertyِ Pname هر Name را نمایش میدهد.
اگر Value در MyTable تغییر کند، اجرای دوباره Code Collection را Reinitialize و Value جدید را میخواند. اگر Object را Static تعریف کنید، Reinitialize نمیشود و Valueهای قبلی Collection حفظ میشوند:
Sub test_class()
Static pn As New MyPNames, n As Integer
MsgBox pn.Count
For n = 1 To pn.Count
MsgBox pn.Item(n).Pname
Next n
End Sub
در پایان، برای محافظت Codeِ Object Model روی Project Properties کلیک، Protection Tab را باز، Lock Project for Viewing را فعال و Password تعیین کنید. Password را فراموش نکنید.
استفاده از PNames بهعنوان Multitier Application
پس از ساخت Object Model مبتنی بر Data Services Layer، Access Database دیگری بسازید که نقش User Interface Application را داشته باشد. در VBE با Tools | References و Browse، Databaseِ Object Model قبلی را پیدا کنید؛ چون DLL/OCX نیست در List عادی ظاهر نمیشود. Reference را فعال، Save، Database را Close و دوباره Open کنید.
اکنون Object Model در Project Window دیده میشود. Module جدید بسازید:
Sub test_class()
Dim pn As MyPnames, n As Integer
Set pn = New MyPnames
MsgBox pn.Count
For n = 1 To pn.Count
MsgBox pn.Item(n).Pname
Next n
End Sub
Message Box ابتدا Count Recordها و سپس Pname هر Record را نشان میدهد. User Interface از طریق Object Modelِ Referenceشده به Data Services Layer پشت آن دسترسی پیدا میکند و معماری چندلایه شکل میگیرد.
فصل ۲۲ — Animation
در Access نیز میتوان Objectها را Animate کرد. در حالت افراطی حتی امکان ساخت بازی سادهٔ Arcade وجود دارد، هرچند برای Application جدی مناسب نیست. مثال فصل یک Label را روی Form حرکت میدهد تا به Borderها برخورد کند و جهتش عوض شود؛ رفتاری شبیه بازی کلاسیک Breakout.
یک Blank Form بسازید، آن را در Design View باز و Label Control اضافه کنید. Name را به MyLabel، Caption را مثلاً My Text و FontSize را 18 تغییر دهید. سپس روی فضای Form راستکلیک، Build Event و Code Builder را انتخاب کنید.
شکل ۲۲-۱ — آمادهسازی Form برای Animation
Code را به Form Open Event اضافه کنید:
Private Sub Form_Open(Cancel As Integer)
Dim VIncr As Long, HIncr As Long, Spd As Integer
Spd = 15
VIncr = Spd
HIncr = Spd
Do Until 1 = 2
If Me.MyLabel.Left + HIncr < 0 Then HIncr = Spd
If Me.MyLabel.Left > Me.InsideWidth - Me.MyLabel.Width Then HIncr = -Spd
If Me.MyLabel.Top + VIncr < 0 Then VIncr = Spd
If Me.MyLabel.Top > Me.InsideHeight - Me.MyLabel.Height Then VIncr = -Spd
Me.MyLabel.Left = Me.MyLabel.Left + HIncr
Me.MyLabel.Top = Me.MyLabel.Top + VIncr
Me.Repaint
Loop
End Sub
این Code بهصورت عمدی Loop بینهایت دارد و تا CTRL+BREAK، بستهشدن Form یا اضافهکردن Condition توقف ادامه مییابد. چون به Form_Open متصل است، بلافاصله پس از بازشدن Form آغاز میشود.
HIncr Increment افقی، VIncr Increment عمودی و Spd Speed حرکت را نگه میدارند. Sign مثبت یا منفی HIncr/VIncr Direction را نیز تعیین میکند. ابتدا هر دو برابر Spd هستند.
شرطهای If برخورد Label با Borderهای چپ، راست، بالا و پایین را بررسی میکنند. هنگام برخورد، Signِ Increment مناسب تغییر میکند تا Direction معکوس شود. Me نمایندهٔ Form جاری است.
برای Border چپ، اگر Left + HIncr کمتر از صفر شود، Increment افقی Positive میشود تا Label به راست حرکت کند. برای Border راست، Position با InsideWidth - MyLabel.Width مقایسه میشود. استفاده از Width/Height خود Label مانع میشود Control هنگام نزدیکشدن به Bottom یا Right برای لحظهای از Form خارج شود.
پس از محاسبهٔ Increment جدید، Propertyهای Left و Top افزایش داده میشوند و Me.Repaint Form را Refresh میکند تا Position جدید دیده شود.
Form را در Form View باز کنید؛ Label تا زمان توقف در Form حرکت و Bounce میکند. همین تکنیک میتواند برای Splash Screen، Screen انتظار در Process طولانی یا با جایگزینی Picture Control بهجای Label برای حرکت Graphic Image استفاده شود.
صفحهٔ پایانی فصل در نسخهٔ اصلی عمداً خالی است.