همانطور که در قسمت قبل گفته شد، در این قسمت با روش کار jQuery Mobile و pluginهای مربوط به Cordova آشنا خواهیم شد.
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Title</title> <meta name="viewport" content="width=device-width, initial-scale=1">
روال کار jQuery Mobile
از آنجایی که مستندات jQuery Mobile به قدر کافی کامل هست، نیازی نیست تا در مورد تک تک آنها مثال بزنیم و از اصل مطلب دور شویم. در هر مثالی که زده خواهد شد، در صورت استفاده از ویجتی خاص، با آن آشنا خواهیم شد.
لیست کامل اتریبیوتهای -data به همراه مقادیری که میپذیرند
شما میتوانید از امکانات Theme Roller برای شخصی سازی تمهای مورد نیاز استفاده کنید.
Cordova Plugins
از این قسمت http://plugins.cordova.io/#/viewAll و این قسمت http://plugreg.com/plugins میتوانید سراغ پلاگینهای مورد نیاز خود بگردید. برای مثال وارد بخش کانفیگ پروژه شده و از قسمت plugins و تب Core یکسری از پلاگینهایی را که در Cordova گنجانده شده است، مشاهده میکنید. با کلیک بر روی دکمهی Add میتوانید آن را دانلود کرده و از APIهای آن استفاده کنید.
برای مثال پلاگین Notification را به پروژه اضافه میکنم. سپس یک فایل js را با نام custom.js به فولدر scripts در ریشه پروژه اضافه کرده و محتوای فایلهای index.html , custome.js را به شکل زیر در نظر میگیرم:
$(function() { $("#alert").on('tap', function(event) { navigator.notification.alert("اطلاعات ذخیره شد",null, "alert", "تایید"); }); $("#prompt").on('tap', function(event) { navigator.notification.prompt("برای تائید نام خود را وارد کنید", onPrompt, "prompt", "تایید", "لغو"],"نام خود"]); }); function onPrompt(results) { navigator.notification.alert(results.buttonIndex + "\n" + results.input1, null); } $("#confirm").on('tap', function(event) { navigator.notification.confirm("حذف انجام شود؟", onConfirm, "confirm", ["بله", "خیر", "نمیدانم"]); }); function onConfirm(buttonIndex) { navigator.notification.alert(buttonIndex , null); } $("#beep").on('tap', function(event) { navigator.notification.beep(1); }); });
رخداد tap زمانی صادر میشود که کاربر، دکمهی مورد نظر را لمس کند و یکی از رخدادهای jQuery Mobile میباشد. بعد از نصب پلاگین Notification، با استفاده از navigator.notification میتوانید به متدهای مورد نظر که در بالا مشخص است، دسترسی پیدا کنید.
برای آشنایی با این پلاگین میتوانید داکیومنت آن را مطالعه کنید.
در کد بالا با استفاده از متدهای callback توانستهایم اطلاعاتی در مورد نوع عملکرد کاربر با notification ما بدست آوریم.
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>CordovaApp01</title> <meta name="viewport" content="width=device-width, initial-scale=1"/> <!-- CordovaApp01 references --> <link href="css/index.css" rel="stylesheet" /> <link href="jquery.mobile.rtl/css/themes/default/rtl.jquery.mobile-1.4.0.css" rel="stylesheet" /> </head> <body> <div data-role="page" id="page1"> <div data-role="header"> <h2> تست پلاگین Notification </h2> </div> <div data-role="content"> <a href="#page2" data-transition="pop" data-rel="dialog" data-role="button" data-inline="true" data-icon="back">page 2</a> <button data-role="button" id="alert" data-inline="true" >alert</button> <button data-role="button" id="confirm" data-inline="true">confirm</button> <button data-role="button" id="beep" data-inline="true" >beep</button> <button data-role="button" id="prompt" data-inline="true" >prompt</button> </div> <div data-role="footer"> <h2>من فوتر هستم</h2> </div> </div> <div data-role="page" id="page2"> <div data-role="header"> <h1>Header</h1> </div> <div data-role="content"> Content </div> <div data-role="footer"> <h1>Footer</h1> </div> </div> <!-- Cordova reference, this is added to your app when it's built. --> <script src="scripts/jquery-2.1.3.min.js"></script> <script src="cordova.js"></script> <script src="scripts/platformOverrides.js"></script> <script src="scripts/index.js"></script> <script src="jquery.mobile.rtl/js/rtl.jquery.mobile-1.4.0.js"></script> <script src="scripts/custom.js"></script> </body> </html>
در کد بالا 4 تا button دیده میشود که ویژگی data-role آنها مقدار button در نظر گرفته شدهاست تا توسط jQuery Mobile به عنوان button شناخته شوند و استایلهای لازم بر روی آنها اعمال گردد. قرار است طبق کد js ایی که نوشتهایم، با لمس کردن هر کدام از دکمهها، notification هایی نمایش داده شوند.
برای اینکار شبیه ساز YouWave را دانلود کرده و نصب کنید. سپس در قسمت toolbar ویژوال، گزینهی Device را به جای شبیه ساز Ripple انتخاب کنید. نرم افزار youwave را اجرا کنید حال اگر برنامه را اجرا کنید با خطای زیر مواجه خواهید شد:
Error447C:\Users\Administrator\Documents\Visual Studio 2013\Projects\CordovaApp-01\CordovaApp-01\bld\Debug\platforms\android\cordova\node_modules\q\q.js:126CordovaApp-01 Error448throw e;CordovaApp-01 Error449^CordovaApp-01 Error450Error : DEP10201 : Failed to deploy to device, no devices found.CordovaApp-01
adb connect localhost:5558
<a href="#page2" data-transition="pop" data-rel="dialog" data-role="button" data-inline="true" data-icon="back">page 2</a>
در مقالهی بعد، به مباحث Database در Cordova خواهیم پرداخت.
ادامه دارد...
عموما محدود کردن دسترسی بر اساس IP بهتر است بر اساس راه حلهایی مانند فایروال، IPSec و یا RRAS IP Filter صورت گیرد که جزو بهینهترین و امنترین راه حلهای ممکن هستند.
در ادامه قصد داریم این محدودیت را با استفاده از امکانات خود اس کیوال سرور انجام دهیم (بلاک کردن کاربران بر اساس IP های غیرمجاز). مواردی که در ادامه ذکر خواهند شد در مورد اس کیوال سرور 2005 ، سرویس پک 2 به بعد و یا اس کیوال سرور 2008 صادق است.
اس کیوال سرور این قابلیت را دارد که میتوان بر روی کلیه لاگینهای صورت گرفته در سطح سرور تریگر تعریف کرد. به این صورت میتوان تمامی لاگینها را برای مثال لاگ کرد (جهت بررسی مسایل امنیتی) و یا میتوان هر لاگینی را که صلاح ندانستیم rollback نمائیم (ایجاد محدودیت روی لاگین در سطح سرور).
لاگ کردن کلیه لاگینهای صورت گرفته به سرور
ایجاد جدولی برای ذخیره سازی اطلاعات لاگینها:
USE [master]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_PADDING ON
GO
CREATE TABLE [dbo].[Logging](
[id] [int] IDENTITY(1,1) NOT NULL,
[LogonTime] [datetime] NULL,
[LoginName] [nvarchar](max) NULL,
[ClientHost] [varchar](50) NULL,
[LoginType] [varchar](100) NULL,
[AppName] [nvarchar](500) NULL,
[FullLog] [xml] NULL,
CONSTRAINT [PK_IP_Log] PRIMARY KEY CLUSTERED
(
[id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
SET ANSI_PADDING OFF
GO
ALTER TABLE [dbo].[Logging] ADD CONSTRAINT [DF_IP_Log_LogonTime] DEFAULT (getdate()) FOR [LogonTime]
GO
در ادامه یک تریگر لاگین را جهت ذخیره سازی اطلاعات کلیه لاگینها به سرور ایجاد مینمائیم:
USE [master]
GO
CREATE TRIGGER LogonTrigger
ON ALL SERVER
FOR LOGON
AS
BEGIN
DECLARE @data XML
SET @data = EVENTDATA()
INSERT INTO [Logging]
(
[LoginName],
[ClientHost],
[LoginType],
[AppName],
[FullLog]
)
VALUES
(
@data.value('(/EVENT_INSTANCE/LoginName)[1]', 'nvarchar(max)'),
@data.value('(/EVENT_INSTANCE/ClientHost)[1]', 'varchar(50)'),
@data.value('(/EVENT_INSTANCE/LoginType)[1]', 'varchar(100)'),
APP_NAME(),
@data
)
END
SELECT TOP 100 * FROM [master].[dbo].[Logging] ORDER BY id desc
محدود کردن کاربران بر اساس IP
ClientHost ایی که در رخداد لاگین فوق بازگشت داده میشود همان IP کاربر راه دور است. برای فیلتر کردن IP های غیرمجاز، ابتدا در دیتابیس مستر یک جدول برای ذخیره سازی IP های مجاز ایجاد میکنیم و IP های کلیه کلاینتهای معتبر خود را در آن وارد میکنیم:
USE [master]
GO
CREATE TABLE [IP_RESTRICTION](
[ValidIP] [varchar](15) NOT NULL,
CONSTRAINT [PK_IP_RESTRICTION] PRIMARY KEY CLUSTERED
(
[ValidIP] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
سپس تریگر لاگین ما برای منع کاربران غیرمجاز بر اساس IP ، به صورت زیر خواهد بود:
USE [master]
GO
CREATE TRIGGER [LOGIN_IP_RESTRICTION]
ON ALL SERVER
FOR LOGON
AS
BEGIN
DECLARE @host NVARCHAR(255);
SET @host = EVENTDATA().value('(/EVENT_INSTANCE/ClientHost)[1]', 'nvarchar(max)');
IF (
NOT EXISTS(
SELECT *
FROM MASTER.dbo.IP_RESTRICTION
WHERE ValidIP = @host
)
)
BEGIN
ROLLBACK;
END
END;
تریگر فوق خطرناک است! ممکن است خودتان هم دیگر نتوانید لاگین کنید!! (حتی با اکانت ادمین)
بنابراین قبل از لاگین حتما IP لوکال و یا ClientHost لوکال را هم وارد کنید.
اگر گیر افتادید به صورت زیر میشود رفع مشکل کرد:
تنها حالتی که تریگر لاگین را فعال نمیکند Dedicated Administrator Connection است یا DAC هم به آن گفته میشود. به صورت پیش فرض برای ایجاد این اتصال اختصاصی باید به کامپیوتری که اس کیوال سرور بر روی آن نصب است به صورت لوکال لاگین کرد و سپس در خط فرمان دستور زیر را صادر کنید (حرف A آن باید بزرگ باشد):
C:\>sqlcmd -A -d master -q "insert into IP_RESTRICTION(validip) values('<local machine>')"
این نوع تریگرها در قسمت server objects در management studio ظاهر میشوند.
مروری بر نحوهی کارکرد مسیریابی اصلی برنامه
به router-outlet ایی که در فایل قالب src\app\app.component.html قرار گرفتهاست، primary outlet میگویند. زمانیکه کاربر، برنامه را در مرورگر مشاهده میکند، با هربار کلیک بر روی یکی از لینکهای منوی بالای سایت، قالب آنرا در این primary outlet مشاهده میکند. اگر بخواهیم پنل دیگری را در همین صفحه و در همین سطح از نمایش، درج کنیم، نیاز به تعریف outlet دیگری است که به همراه مسیرهای ثانویهای نیز خواهد بود.
تعریف یک router-outlet نامدار
با توجه به اینکه هر پنل به همراه مسیریابی ثانویه، نیاز به router-outlet خودش را خواهد داشت، مسیریاب برای اینکه بداند محتوای آنها را در کجای صفحه درج کند، به نامهای آنها مراجعه میکند. به این ترتیب میتوان چندین router-outlet را در یک سطح از نمایش تعریف کرد؛ اما هرکدام باید دارای نامی منحصربفرد باشند.
در مثال این سری میخواهیم پنلی را در سمت راست صفحهی اصلی درج کنیم. برای تعریف آن در همان سطحی که router-outlet اصلی قرار دارد، نیاز است فایل src\app\app.component.html را ویرایش کنیم:
<div class="container"> <div class="row"> <div class="col-md-10"> <router-outlet></router-outlet> </div> <div class="col-md-2"> <router-outlet name="popup"></router-outlet> </div> </div> </div>
افزودن ماژول جدید پیامهای سیستم
در ادامه ماژول جدید پیامهای سیستم را به همراه تنظیمات ابتدایی مسیریابی آن اضافه خواهیم کرد که در آن ماژول، مدیریت نمایش پیامهای مختلفی در router-outlet ثانویه popup صورت خواهد گرفت:
>ng g m message --routing
در ادامه نیاز است MessageModule را به قسمت imports فایل src\app\app.module.ts نیز معرفی کنیم (پیش از AppRoutingModule که حاوی مسیریابی catch all است):
import { MessageModule } from './message/message.module'; @NgModule({ declarations: [ ], imports: [ BrowserModule, FormsModule, HttpModule, InMemoryWebApiModule.forRoot(ProductData, { delay: 1000 }), ProductModule, UserModule, MessageModule, AppRoutingModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
سپس کامپوننت جدید Message را به ماژول Message برنامه اضافه میکنیم:
>ng g c message/message
پس از آن یک سرویس ابتدایی پیامهای کاربران را نیز اضافه خواهیم کرد:
>ng g s message/message -m message/message.module
installing service create src\app\message\message.service.spec.ts create src\app\message\message.service.ts update src\app\message\message.module.ts
پس از ایجاد قالب ابتدایی فایل message.service.ts آنرا به نحو ذیل تکمیل میکنیم:
import { Injectable } from '@angular/core'; @Injectable() export class MessageService { private messages: string[] = []; isDisplayed = false; addMessage(message: string): void { let currentDate = new Date(); this.messages.unshift(message + ' at ' + currentDate.toLocaleString()); } }
اکنون جهت تکمیل کامپوننت پیامها، ابتدا فایل قالب message.component.html را به نحو ذیل تکمیل میکنیم:
<div class="row"> <h4 class="col-md-10">Message Log</h4> <span class="col-md-2"> <a class="btn btn-default" (click)="close()">x</a> </span> </div> <div *ngFor="let message of messageService.messages; let i=index"> <div *ngIf="i<10" class="message-row"> {{ message }} </div> </div>
کدهای کامپوننت این قالب به صورت ذیل است:
import { MessageService } from './../message.service'; import { Router } from '@angular/router'; import { Component, OnInit } from '@angular/core'; @Component({ //selector: 'app-message', templateUrl: './message.component.html', styleUrls: ['./message.component.css'] }) export class MessageComponent implements OnInit { constructor(private messageService: MessageService, private router: Router) { } ngOnInit() { } close(): void { // Close the popup. this.router.navigate([{ outlets: { popup: null } }]); this.messageService.isDisplayed = false; } }
تکمیل سایر کامپوننتهای برنامه در جهت استفاده از سرویس پیامها
ابتدا به فایل src\app\product\product-edit\product-edit.component.ts مراجعه کرده و سرویس جدید پیامها را به سازندهی آن تزریق میکنیم:
import { MessageService } from './../../message/message.service'; @Component({ selector: 'app-product-edit', templateUrl: './product-edit.component.html', styleUrls: ['./product-edit.component.css'] }) export class ProductEditComponent implements OnInit { constructor(private productService: ProductService, private messageService: MessageService, private route: ActivatedRoute, private router: Router) { }
onSaveComplete(message?: string): void { if (message) { this.messageService.addMessage(message); }
تنظیم مسیرهای ثانویه
نحوهی تعریف مسیریابیهای مرتبط با router-outletهای غیراصلی برنامه، همانند سایر مسیریابیهای برنامهاست؛ با این تفاوت که در اینجا خاصیت outlet نیز به تنظیمات مسیر اضافه خواهد شد. به این ترتیب مشخص خواهیم کرد که محتوای این مسیر باید دقیقا در کدام router-outlet نامدار، درج شود.
برای این منظور فایل src\app\message\message-routing.module.ts را گشوده و تنظیمات مسیریابی آنرا که به صورت RouterModule.forChild تعریف میشوند (چون ماژول اصلی برنامه نیستند)، تکمیل خواهیم کرد:
const routes: Routes = [ { path: 'messages', component: MessageComponent, outlet: 'popup' } ];
فعالسازی یک مسیر ثانویه
در اینجا نیز همانند سایر مسیریابیها، از دایرکتیو routerLink برای فعالسازی مسیرهای ثانویه استفاده میکنیم؛ اما syntax آن کمی متفاوت است:
<a [routerLink]="[{ outlets: { popup: ['messages'] } }]">Messages</a> <a [routerLink]="['/products', product.id, 'edit', { outlets: { popup: ['summary', product.id] } }]">Messages</a>
در دومین لینک تعریف شده، ابتدا یک مسیر اصلی فعال شده و سپس یک مسیر ثانویه نمایش داده میشود.
یک نکته: هرچند به primary outlet نامی انتساب داده نمیشود، اما نام آن دقیقا primary است و میتوان قسمت outlets را به صورت ذیل نیز تعریف کرد:
{ outlets: { primary: ['/products', product.id,'edit'], popup: ['summary', product.id] }}
در ادامه فایل src\app\app.component.html را ویرایش کرده و لینک Show Messages را به آن اضافه میکنیم:
<ul class="nav navbar-nav navbar-right"> <li *ngIf="authService.isLoggedIn()"> <a>Welcome {{ authService.currentUser.userName }}</a> </li> <li> <a [routerLink]="[{ outlets: { popup: ['messages'] } }]">Show Messages</a> </li>
آدرس آن نیز چنین شکلی را پیدا میکند:
http://localhost:4200/products(popup:messages)
اکنون میخواهیم قابلیت مخفی سازی این پنل را نیز پیاده سازی کنیم. به همین جهت از خاصیت isDisplayed سرویس پیامها که توسط دکمهی بستن MessageComponent مدیریت میشود، استفاده خواهیم کرد. بنابراین لینک جدیدی را که در فایل src\app\app.component.html اضافه کردیم، به نحو ذیل تغییر خواهیم داد:
<li *ngIf="!messageService.isDisplayed"> <a (click)="displayMessages()">Show Messages</a> </li> <li *ngIf="messageService.isDisplayed"> <a (click)="hideMessages()">Hide Messages</a> </li>
import { MessageService } from './message/message.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { constructor(private authService: AuthService, private router: Router, private messageService: MessageService) { } displayMessages(): void { this.router.navigate([{ outlets: { popup: ['messages'] } }]); this.messageService.isDisplayed = true; } hideMessages(): void { this.router.navigate([{ outlets: { popup: null } }]); this.messageService.isDisplayed = false; } }
برای فعالسازی یک مسیرثانویه توسط متدهای برنامه، نیاز است از سرویس مسیریاب و متد navigate آن استفاده کرد که نمونههایی از آنرا در اینجا ملاحظه میکنید. پارامترهای ذکر شدهی در اینجا نیز همانند دایرکتیو routerLink هستند.
یک نکته: اگر به متد hideMessages دقت کنید، مقدار value کلید popup به نال تنظیم شدهاست. این مورد سبب خواهد شد تا outlet آن خالی شود. به این ترتیب متد hideMessages علاوه بر مخفی کردن لینک نمایش پیامها، پنل آنرا نیز از صفحه حذف میکند. شبیه به همین نکته در متد close کامپوننت پیامها که دکمهی بستن آنرا به همراه دارد، پیاده سازی شدهاست.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-routing-lab-07.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng s -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
- حالت ارجاع : شماره سند یا Object Id را شامل شده و در صورتیکه به اطلاعاتی نیاز داشتید، باید اطلاعات آن را در یک درخواست جداگانه واکشی نمایید. چون مونگو شامل جوین نبوده و جوینها باید در سطح اپلیکیشن مدیریت شوند.
{ fname:'ali', lname:'yeganeh', accounts:[454354353,3455435] }
- حالت جاسازی سند (یا اسناد تو در تو) Embed : در این حالت سند مورد نظر اطلاعات سند دیگری را در درون خود نگه میدارد. در این حالت به هیچ جوینی نیازی نیست و اطلاعات وابسته، به همراه خود سند اصلی واکشی میشوند. این نکته باید مورد توجه قرار بگیرد که مونگو یک دیتابیس غیر اتمیک هست و در صورتیکه اصل دیتا تغییر کند، تغییر یا به روزرسانی در سندهای Embed انجام نخواهد شد و در صورت نیاز باید خودتان به طور دستی آن را کنترل نمایید.
{ fname:'ali', lname:'yeganeh', accounts:[ { username:"ali", password:"123" }, { username:"reza", password:"456" } ] }
book { name:'Scarlet Letter", Language:"English", Pages:124, ... } publisher { name : "Orielly", ... }
book { name:'Scarlet Letter", Language:"English", Pages:124, ..., publisher: { name : "Orielly", ... } }
- در این حالت در صورتیکه واکشی هر کتاب به همراه اطلاعات ناشر را نیاز داشته باشیم و یا پرس وجوهای ترکیبی نیاز باشد، در سریعترین زمان ممکن واکشی انجام خواهد شد.
- درج و مدیریت آن راحتتر خواهد بود.
- در صورتیکه اطلاعات ناشر نیاز به تغییرات اساسی داشته باشد و باید در تمامی سندها اصلاح گردد، باید تمامی اسناد مربوط به اطلاعات کتاب به روزرسانی شوند که هزینه سنگینتری را خواهد داشت.
- دیتای تکراری زیادی ذخیره خواهد شد و در نتیجه حافظه بیشتری را میطلبد.
- در صورتیکه تنها به اطلاعات ناشر نیاز باشد و اطلاعات ناشر در سند دیگری وجود نداشته باشد و فقط در سند کتاب وجود داشته باشد، واکشی آن هزینه سنگینتری را خواهد طلبید. به همین جهت توصیه میشود در صورتیکه دیتای شما میتواند به صورت یک موجودیت مستقل هم عمل کند، اطلاعات آن در سند دیگری که من به آن سند اصلی میگویم ذخیره شوند تا نمونهها از روی آخرین ویرایش آن ساخته شوند و موقعیکه تنها به واکشی آن اطلاعات نیاز است، همانها بیرون کشیده شوند.
book { name:'Scarlet Letter", Language:"English", Pages:124, ..., publisher:1212121 }
- عدم وجود تکرار اطلاعات
- چون تنها یک سند برای ویرایش وجود دارد، نیازی به اصلاح اسناد توکار نیست و ویرایش، هزینه کمتری خواهد داشت.
- عدم وجود جوین: در صورتیکه نیاز به جوین بزرگی باشد، این نوع جوین باید در سطح برنامه شما انجام شود و هزینه بر خواهد بود.
Post { title:"C#", body:"About C#", tags:['C#','.Net','microsoft'], Categories:[{name:'Programming'}], votes:[{rate:3,user:42342},{rate:5,user:423445},...], comments:[ { text:"my comment1", time:"10/2/1396",...}, ... ] }
{ POST:45453, count:35, comments:[...] }
{ post:345345, capacity:100, count:35, bucket:2, comments:[...] }
- همیشه به این نکته توجه داشته باشید که نباید بگذارید تعداد آرایههای یک سند خیلی بزرگ شوند. در غیر اینصورت کارآیی مونگو به خصوص در حین ویرایش سند پایین خواهد آمد. در حین ویرایش، اگر سندی از اندازهی خود بزرگتر نشود، مشکلی پیش نمیاید ولی اگر فضایی بیش از آنچه که قبلا داشته به آن اضافه شود، سند نیاز به جابجایی و گسترش فضا خواهد داشت. در این حالت باید مونگو سند را به جای دیگری که فضای کافی برای آن وجود دارد، انتقال بدهد و میزان Disk Fragment به طبع بالا خواهد رفت. همچنین اندیسهای آرایهای هم با جابجا شدن دیتا نیاز به، به روزرسانی خواهند داشت و زمانی هم صرف به روزرسانی اندیسها خواهد شد.
- مدیر محصول مونگو اظهار نظر صریحی در این مورد نکردهاست، ولی به نظر میرسد نوع فرمت BSON از یک اسکن خطی در حافظه استفاده میکند و زمان بیشتری صرف پیدا کردن المانهای انتهایی در آرایه خواهد شد؛ پس بیشتر عملیات در این نوع سند، با کندی مواجه خواهند شد. با توجه به کامنتهایی که در سایتها و شبکههای اجتماعی یافت شدهاست، آرایه ای با بیش از صدهزار آیتم ساده میتواند آسیب زا باشد؛ به همین دلیل توصیه میشود که اگر بیش از صدهزار آیتم نیاز است، از همان حالت Bucket استفاده شود.
- استفاده از اندیسها هم سابقهی دیرینهای داشته و سعی کنید کوئری هایی بزنید که بر اساس اندیسهای تعریف شده باشند تا واکشی دیتا سریعتر شود. پس نحوه کوئری نویسی و انتخاب فیلدی که اندیس میشود بسیار مهم است.
- استفاده از Projection تاثیری بر خواندن اسناد ندارد و هر سند به طور کامل واکشی میشود. projection تنها در بارهی ترافیک یا انتقال حجم کمتری از اطلاعات به سمت کلاینت تاثیرگذار میباشد. پس استفاده از projection بجای جدا سازی اسناد را دنبال نکنید.
یازده. در جاوا رویدادها با استفاده از اینترفیسها پیاده سازی میشوند. برای نامگذاری یک رویداد، قاعده آن در جاوا بدین شکل است که نامها به صورت (+ ) Camel نوشته شده و آخرین عبارت هم Listener باشد و نیازی هم به حرف I در نامگذاری اینترفیس نیست؛ چون همه میدانند که این Listener آخری یعنی رویدادی که با اینترفیس پیاده سازی شده است و استفاده از I بی معنی است. هر چند بر خلاف دات نت، در اینجا استفاده از قاعده I چندان متداول نیست.
public interface CopyFileListener { void PublishProgress(long fileSize,long copiedSize); }
دوازده. گوگل اینترفیسهایی را که برای رویدادها میسازد، داخل کلاس اصلی تعریف میکند. پس بهتر هست که شما هم همین روند را ادامه بدید و از این قاعده خارج نشوید. اگر خوب دقت کرده باشید، در برنامه نویسی اندروید تمام اینترفیسها داخل کلاس اصلی هستند:
textView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { } });
public class MemoryWare { public interface CopyFileListener { void PublishProgress(long fileSize,long copiedSize); } .... }
SetOnClickListener
editText.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { } });
سیزده. آداپتورها و آداپتور ویوها (چون لیست) قسمت مهمی از برنامههای اندرویدی به شمار میآیند؛ تا حدی که در بیشتر برنامههای ساده هم حضور پررنگی دارند. ولی برای استفاده از این آداپتورها باید بدانید که نحوه کار آنها چگونه است. بسیاری از کاربران در این قسمت اشتباهات زیادی میکنند. اگر در stackOverflow هم در اینباره نگاه کنید، با حجم انبوهی از سوالات روبرو میشوید و فقط به خاطر اینکه نحوه کارکرد آن را نمیدانند، به مشکل برخوردهاند.
کلاس BaseAdapter اصلیترین کلاس آداپتور هاست که بقیه از آن مشتق شدهاند و معروفترین مشتقات آن، کلاسهای CursorAdapter و ArrayAdapter هستند که امکانات بیس آداپتور را افزایش دادهاند.به عنوان مثال در کد پایین از ArrayAdapter استفاده شده است.
نحوه کار یک آداپتور بدین صورت است که متدی را به نام GetView با قابلیت override دارد که با هر تعداد آیتم موجود صدا زده میشود. ولی اگر تصور کنیم فقط چند صدهزار آیتم هم داشته باشیم، آیا واقعا اجرا میشود؟ جواب این سوال این است که با هر بار اسکرولی که شما میکنید آیتمهای بعدی ایجاد میشوند ولی باز این سوال پیش میآید که هر آیتم برای خود جداگانه تشکیل میشود؟ مطمئنا جواب خیر است. آداپتورها از سیستمی به نام ViewRecycler برای کش کردن آیتمهای ایجاد شده استفاده میکنند و با هر اسکرولی که انجام میشود آیتمهای بعدی از روی آیتمهای قبلی که قبلا از صفحه خارج شدهاند، ساخته میشوند و آیتمهای کش شده قبلی را با پارامتری با نام convertView به دست شما میرساند.
کد زیر را ببینید:
@Override public View getView(int position, View rowView, ViewGroup parent) { ViewHolder viewHolder=null; if(rowView==null) { // 1. Create inflater LayoutInflater inflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); // 2. Get rowView from inflater rowView = inflater.inflate(R.layout.row_bank_group_list, parent, false); viewHolder=new ViewHolder(); viewHolder.txtGroupName=(TextView) rowView.findViewById(R.id.text_groupName); rowView.setTag(viewHolder); } else { viewHolder=(ViewHolder)rowView.getTag(); } viewHolder.txtGroupName.setText(getItem(position).getName()); viewHolder.txtGroupName.setTypeface(new FontSystem().get_General_Font(context)); viewHolder.txtGroupName.setTextColor(context.getResources().getColor(R.color.black)); return rowView; }
کلاس داخلی ViewHolder هم یک الگو برای عدم بررسی Viewهای داخل آن است که نیازی به یافتن و تبدیل مجدد آنها نداشته باشید. در این روش شیء، داخل خصوصیت tag آیتم قرار گرفته است و وقتی از کش برداشته شود، خاصیت تگ آن را میخوانیم و مستقیما مورد استفاده قرار میدهیم. در این حالت شما بهترین استفاده را از پردازشها و حافظه، میکنید.
چهارده. یکی از کارهایی را که قبل از کار کردن در یک مسیر فیزیکی باید انجام دهید این است که مطمئن باشید اجازه نوشتن در آن ناحیه را دارید یا خیر. در غیر اینصورت برنامه شما با خطای FC روبرو میشود و اجرای آن خاتمه مییابد. به همین دلیل اکثر برنامه نویسان از متد CanWrite در کلاس File استفاده میکنند. ولی در هنگام استفاده از این متد باید دقت داشته باشید که کلاس File فقط باید حاوی مسیر باشد و اسمی از فایل مربوطه در آن نباشد. دلیل هم آن است که این احتمال میرود اگر فایلی هم وجود نداشته باشد، مقدار false را به شما برگرداند. مثال زیر قرار است فایلی را در کارت حافظه بنویسید، ولی بررسی اجازه نوشتن در مسیر، اشتباه است:
File file=new File(sdcardPath,fileName); if(file.CanWrite()) { ..... }
File file=new File(sdcardPath); if(file.CanWrite()) { file=new File(sdcardPath,filePath); ..... }
پانزده. کارت حافظه خارجی: همه برنامه نویسان اندروید حداقل یکبار از کد زیر استفاده کرده اند:
Environment.getExternalStorageDirectory()
هر برنامهای که در اندروید نصب میشود در مسیر
/Data/Data
/Data/Data/Info.Dotnettips.MyApp
/** * it will returns sd path for you * <p> * <b>Required Permission: </b>android.permission.READ_EXTERNAL_STORAGE<br/> * </p> * @return */ public List<String> GetExternalMounts() { final List<String> out = new ArrayList<>(); String reg = "(?i).*vold.*(vfat|ntfs|exfat|fat32|ext3|ext4).*rw.*"; String s = ""; try { final Process process = new ProcessBuilder().command("mount") .redirectErrorStream(true).start(); process.waitFor(); final InputStream is = process.getInputStream(); final byte[] buffer = new byte[1024]; while (is.read(buffer) != -1) { s = s + new String(buffer); } is.close(); } catch (final Exception e) { e.printStackTrace(); } // parse output final String[] lines = s.split("\n"); for (String line : lines) { if (!line.toLowerCase(Locale.US).contains("asec")) { if (line.matches(reg)) { String[] parts = line.split(" "); for (String part : parts) { if (part.startsWith("/")) if (!part.toLowerCase(Locale.US).contains("vold")) if(new File(part).canWrite()) out.add(part); } } } } return out; }
شانزده. یکی از روشهای انتقال اطلاعات بین اکتیویتیها مختلف استفاده از Extras هاست که شما با تعیین یک نام یا کلید، اطلاعات مربوطه را ارسال و توسط همان کلید؛ اطلاعات را در اکتیویتی مقصد دریافت میکنید:
notesIntent.putExtra("PartyId", PartyId); startActivity(notesIntent);
PartyId=getIntent().getLongExtra("PartyId",0);
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false); i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false); i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE);
هفده. قواعد نامگذاری: برای نامگذاری متغیرها از قانون CamelCase استفاده میکنیم. ولی برای حالات زیر از روشهای دیگر استفاده میشود:
- برای ثابتها از حروف بزرگ و _ استفاده کنید.
- برای متغیرهای خصوصی از حرف m در ابتدای نام متغیر استفاده کنید.
- برای متغیرهای استاتیک از حرف s در ابتدای نام متغیر استفاده کنید.
public class MyClass { public static final int SOME_CONSTANT = 42; public int publicField; private static MyClass sSingleton; int mPackagePrivate; private int mPrivate; protected int mProtected; }
هجده: قاعده نظم و ترتیب در importها توسط مستندات گوگل بدین شکل تعریف شده است:
- نام پکیجهای ارائه شده توسط گوگل
- نام پکیجهای ثالث
- نام پکیجهای موجود در java و javax
- پکیجهای موجود در پکیج اصلی
نوزدهم. مرتب سازی متدهای دسترسی یک کلاس: بسیار خوب است که همیشه کدهای ما نظم خاصی را داشته باشد تا پیگیریهای شخصی و تیمی در آن راحتتر صورت بگیرد. برای مثال در یک کلاس ابتدا متدهای public و سپس private قرار گیرند و الی آخر.
الگوی عمومی که برای کار با جاوا صورت گرفته است به شکل زیر میباشد:
public, protected, private,abstract, static, transient, volatile, synchronized, final, native.
ادیتور intelij شامل تنظمیاتی برای مرتب سازی کدهاست که در این مورد بسیار سودمند است. با طی کردن مسیر زیر میتوانید برای آن ترتیب اینگونه موارد را مشخص کنید.
Settings>Editor>Code Style>Arrangement
در تصویر بالا متدها به ترتیب متدهای دستری بین بلوکهای کامنت method start و method end قرار گرفته اند.
همچنین شامل گزینههای دیگری نیز میباشد که به نظرم فعال کردنشان بسیار خوب است. گزینه keep overridden methods together به شما کمک میکند تا متدهایی را که رونویسی میشوند، در کنار یکدیگر قرار بگیرند که برای کلاسهای اندرویدی مثل اکتیویتیها و فرگمنتها و ... بسیار خوب است. گزینه مفید دیگر Keep dependent methods together است که در دو حالت عمقی یا خطی متدهای وابسته (متدهایی که متدهای دیگر را در آن کلاس صدا میزنند) در کنار یکدیگر قرار میدهد و مابقی گزینهها، که بسیار سودمند هست. به هر حال هر قاعدهای را که برای خود انتخاب میکنید اگر در حالت پیش فرض نیست بهتر است در مستندات پروژه ذکر شود تا افراد دیگر سریعتر به موضوع پی ببرند.
قسمت بیستم. این مورد برای افراد تازه کار میباشد که تازه اندروید استادیو را باز کردهاند و مشغول کدنویسی میباشند. یکی از مواردی که در همان مرحله اول به آن برمیخورید این است که intellisense ادیتور به بزرگی و کوچکی حروف حساس است و تنها با حرف اول سازگاری دارد. برای تغییر این مسئله باید مسیر زیر را طی کنید:
Settings>Editor>Completion>Case-sensitive Completion>None
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Security.Cryptography; using System.Text; using System.Web.Mvc; using System.Web.Security; namespace OpenIDExample.Models { #region Models public class ChangePasswordModel { [Required] [DataType(DataType.Password)] [Display(Name = "Current password")] public string OldPassword { get; set; } [Required] [ValidatePasswordLength] [DataType(DataType.Password)] [Display(Name = "New password")] public string NewPassword { get; set; } [DataType(DataType.Password)] [Display(Name = "Confirm new password")] [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] public string ConfirmPassword { get; set; } } public class LogOnModel { [Display(Name = "OpenID")] public string OpenID { get; set; } [Required] [Display(Name = "User name")] public string UserName { get; set; } [Required] [DataType(DataType.Password)] [Display(Name = "Password")] public string Password { get; set; } [Display(Name = "Remember me?")] public bool RememberMe { get; set; } } public class RegisterModel { [Display(Name = "OpenID")] public string OpenID { get; set; } [Required] [Display(Name = "User name")] public string UserName { get; set; } [Required] [DataType(DataType.EmailAddress)] [Display(Name = "Email address")] public string Email { get; set; } [Required] [ValidatePasswordLength] [DataType(DataType.Password)] [Display(Name = "Password")] public string Password { get; set; } [DataType(DataType.Password)] [Display(Name = "Confirm password")] [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] public string ConfirmPassword { get; set; } } #endregion Models #region Services // The FormsAuthentication type is sealed and contains static members, so it is difficult to // unit test code that calls its members. The interface and helper class below demonstrate // how to create an abstract wrapper around such a type in order to make the AccountController // code unit testable. public interface IMembershipService { int MinPasswordLength { get; } bool ValidateUser(string userName, string password); MembershipCreateStatus CreateUser(string userName, string password, string email, string OpenID); bool ChangePassword(string userName, string oldPassword, string newPassword); MembershipUser GetUser(string OpenID); } public class AccountMembershipService : IMembershipService { private readonly MembershipProvider _provider; public AccountMembershipService() : this(null) { } public AccountMembershipService(MembershipProvider provider) { _provider = provider ?? Membership.Provider; } public int MinPasswordLength { get { return _provider.MinRequiredPasswordLength; } } public bool ValidateUser(string userName, string password) { if (String.IsNullOrEmpty(userName)) throw new ArgumentException("Value cannot be null or empty.", "userName"); if (String.IsNullOrEmpty(password)) throw new ArgumentException("Value cannot be null or empty.", "password"); return _provider.ValidateUser(userName, password); } public Guid StringToGUID(string value) { // Create a new instance of the MD5CryptoServiceProvider object. MD5 md5Hasher = MD5.Create(); // Convert the input string to a byte array and compute the hash. byte[] data = md5Hasher.ComputeHash(Encoding.Default.GetBytes(value)); return new Guid(data); } public MembershipCreateStatus CreateUser(string userName, string password, string email, string OpenID) { if (String.IsNullOrEmpty(userName)) throw new ArgumentException("Value cannot be null or empty.", "userName"); if (String.IsNullOrEmpty(password)) throw new ArgumentException("Value cannot be null or empty.", "password"); if (String.IsNullOrEmpty(email)) throw new ArgumentException("Value cannot be null or empty.", "email"); MembershipCreateStatus status; _provider.CreateUser(userName, password, email, null, null, true, StringToGUID(OpenID), out status); return status; } public MembershipUser GetUser(string OpenID) { return _provider.GetUser(StringToGUID(OpenID), true); } public bool ChangePassword(string userName, string oldPassword, string newPassword) { if (String.IsNullOrEmpty(userName)) throw new ArgumentException("Value cannot be null or empty.", "userName"); if (String.IsNullOrEmpty(oldPassword)) throw new ArgumentException("Value cannot be null or empty.", "oldPassword"); if (String.IsNullOrEmpty(newPassword)) throw new ArgumentException("Value cannot be null or empty.", "newPassword"); // The underlying ChangePassword() will throw an exception rather // than return false in certain failure scenarios. try { MembershipUser currentUser = _provider.GetUser(userName, true /* userIsOnline */); return currentUser.ChangePassword(oldPassword, newPassword); } catch (ArgumentException) { return false; } catch (MembershipPasswordException) { return false; } } public MembershipCreateStatus CreateUser(string userName, string password, string email) { throw new NotImplementedException(); } } public interface IFormsAuthenticationService { void SignIn(string userName, bool createPersistentCookie); void SignOut(); } public class FormsAuthenticationService : IFormsAuthenticationService { public void SignIn(string userName, bool createPersistentCookie) { if (String.IsNullOrEmpty(userName)) throw new ArgumentException("Value cannot be null or empty.", "userName"); FormsAuthentication.SetAuthCookie(userName, createPersistentCookie); } public void SignOut() { FormsAuthentication.SignOut(); } } #endregion Services #region Validation public static class AccountValidation { public static string ErrorCodeToString(MembershipCreateStatus createStatus) { // See http://go.microsoft.com/fwlink/?LinkID=177550 for // a full list of status codes. switch (createStatus) { case MembershipCreateStatus.DuplicateUserName: return "Username already exists. Please enter a different user name."; case MembershipCreateStatus.DuplicateEmail: return "A username for that e-mail address already exists. Please enter a different e-mail address."; case MembershipCreateStatus.InvalidPassword: return "The password provided is invalid. Please enter a valid password value."; case MembershipCreateStatus.InvalidEmail: return "The e-mail address provided is invalid. Please check the value and try again."; case MembershipCreateStatus.InvalidAnswer: return "The password retrieval answer provided is invalid. Please check the value and try again."; case MembershipCreateStatus.InvalidQuestion: return "The password retrieval question provided is invalid. Please check the value and try again."; case MembershipCreateStatus.InvalidUserName: return "The user name provided is invalid. Please check the value and try again."; case MembershipCreateStatus.ProviderError: return "The authentication provider returned an error. Please verify your entry and try again. If the problem persists, please contact your system administrator."; case MembershipCreateStatus.UserRejected: return "The user creation request has been canceled. Please verify your entry and try again. If the problem persists, please contact your system administrator."; default: return "An unknown error occurred. Please verify your entry and try again. If the problem persists, please contact your system administrator."; } } } [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public sealed class ValidatePasswordLengthAttribute : ValidationAttribute, IClientValidatable { private const string _defaultErrorMessage = "'{0}' must be at least {1} characters long."; private readonly int _minCharacters = Membership.Provider.MinRequiredPasswordLength; public ValidatePasswordLengthAttribute() : base(_defaultErrorMessage) { } public override string FormatErrorMessage(string name) { return String.Format(CultureInfo.CurrentCulture, ErrorMessageString, name, _minCharacters); } public override bool IsValid(object value) { string valueAsString = value as string; return (valueAsString != null && valueAsString.Length >= _minCharacters); } public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context) { return new[]{ new ModelClientValidationStringLengthRule(FormatErrorMessage(metadata.GetDisplayName()), _minCharacters, int.MaxValue) }; } } #endregion Validation }
using System.Web.Mvc; using System.Web.Routing; using System.Web.Security; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OpenId; using DotNetOpenAuth.OpenId.RelyingParty; using OpenIDExample.Models; namespace OpenIDExample.Controllers { public class AccountController : Controller { private static OpenIdRelyingParty openid = new OpenIdRelyingParty(); public IFormsAuthenticationService FormsService { get; set; } public IMembershipService MembershipService { get; set; } protected override void Initialize(RequestContext requestContext) { if (FormsService == null) { FormsService = new FormsAuthenticationService(); } if (MembershipService == null) { MembershipService = new AccountMembershipService(); } base.Initialize(requestContext); } // ************************************** // URL: /Account/LogOn // ************************************** public ActionResult LogOn() { return View(); } [HttpPost] public ActionResult LogOn(LogOnModel model, string returnUrl) { if (ModelState.IsValid) { if (MembershipService.ValidateUser(model.UserName, model.Password)) { FormsService.SignIn(model.UserName, model.RememberMe); if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } else { return RedirectToAction("Index", "Home"); } } else { ModelState.AddModelError("", "The user name or password provided is incorrect."); } } // If we got this far, something failed, redisplay form return View(model); } // ************************************** // URL: /Account/LogOff // ************************************** public ActionResult LogOff() { FormsService.SignOut(); return RedirectToAction("Index", "Home"); } // ************************************** // URL: /Account/Register // ************************************** public ActionResult Register(string OpenID) { ViewBag.PasswordLength = MembershipService.MinPasswordLength; ViewBag.OpenID = OpenID; return View(); } [HttpPost] public ActionResult Register(RegisterModel model) { if (ModelState.IsValid) { // Attempt to register the user MembershipCreateStatus createStatus = MembershipService.CreateUser(model.UserName, model.Password, model.Email, model.OpenID); if (createStatus == MembershipCreateStatus.Success) { FormsService.SignIn(model.UserName, false /* createPersistentCookie */); return RedirectToAction("Index", "Home"); } else { ModelState.AddModelError("", AccountValidation.ErrorCodeToString(createStatus)); } } // If we got this far, something failed, redisplay form ViewBag.PasswordLength = MembershipService.MinPasswordLength; return View(model); } // ************************************** // URL: /Account/ChangePassword // ************************************** [Authorize] public ActionResult ChangePassword() { ViewBag.PasswordLength = MembershipService.MinPasswordLength; return View(); } [Authorize] [HttpPost] public ActionResult ChangePassword(ChangePasswordModel model) { if (ModelState.IsValid) { if (MembershipService.ChangePassword(User.Identity.Name, model.OldPassword, model.NewPassword)) { return RedirectToAction("ChangePasswordSuccess"); } else { ModelState.AddModelError("", "The current password is incorrect or the new password is invalid."); } } // If we got this far, something failed, redisplay form ViewBag.PasswordLength = MembershipService.MinPasswordLength; return View(model); } // ************************************** // URL: /Account/ChangePasswordSuccess // ************************************** public ActionResult ChangePasswordSuccess() { return View(); } [ValidateInput(false)] public ActionResult Authenticate(string returnUrl) { var response = openid.GetResponse(); if (response == null) { //Let us submit the request to OpenID provider Identifier id; if (Identifier.TryParse(Request.Form["openid_identifier"], out id)) { try { var request = openid.CreateRequest(Request.Form["openid_identifier"]); return request.RedirectingResponse.AsActionResult(); } catch (ProtocolException ex) { ViewBag.Message = ex.Message; return View("LogOn"); } } ViewBag.Message = "Invalid identifier"; return View("LogOn"); } //Let us check the response switch (response.Status) { case AuthenticationStatus.Authenticated: LogOnModel lm = new LogOnModel(); lm.OpenID = response.ClaimedIdentifier; //check if user exist MembershipUser user = MembershipService.GetUser(lm.OpenID); if (user != null) { lm.UserName = user.UserName; FormsService.SignIn(user.UserName, false); } return View("LogOn", lm); case AuthenticationStatus.Canceled: ViewBag.Message = "Canceled at provider"; return View("LogOn"); case AuthenticationStatus.Failed: ViewBag.Message = response.Exception.Message; return View("LogOn"); } return new EmptyResult(); } } }
6- سپس برای Action به نام LogOn یک View میسازیم، برای Authenticate نیازی به ایجاد View ندارد چون قرار است درخواست کاربر را به آدرس دیگری Redirect کند. سپس کدهای زیر را برای View ایجاد شده وارد میکنیم.
@model OpenIDExample.Models.LogOnModel @{ ViewBag.Title = "Log On"; } <h2> Log On</h2> <p> Please enter your username and password. @Html.ActionLink("Register", "Register") if you don't have an account. </p> <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script> <form action="Authenticate?ReturnUrl=@HttpUtility.UrlEncode(Request.QueryString["ReturnUrl"])" method="post" id="openid_form"> <input type="hidden" name="action" value="verify" /> <div> <fieldset> <legend>Login using OpenID</legend> <div class="openid_choice"> <p> Please click your account provider:</p> <div id="openid_btns"> </div> </div> <div id="openid_input_area"> @Html.TextBox("openid_identifier") <input type="submit" value="Log On" /> </div> <noscript> <p> OpenID is service that allows you to log-on to many different websites using a single indentity. Find out <a href="http://openid.net/what/">more about OpenID</a> and <a href="http://openid.net/get/">how to get an OpenID enabled account</a>.</p> </noscript> <div> @if (Model != null) { if (String.IsNullOrEmpty(Model.UserName)) { <div class="editor-label"> @Html.LabelFor(model => model.OpenID) </div> <div class="editor-field"> @Html.DisplayFor(model => model.OpenID) </div> <p class="button"> @Html.ActionLink("New User ,Register", "Register", new { OpenID = Model.OpenID }) </p> } else { //user exist <p class="buttonGreen"> <a href="@Url.Action("Index", "Home")">Welcome , @Model.UserName, Continue..." </a> </p> } } </div> </fieldset> </div> </form> @Html.ValidationSummary(true, "Login was unsuccessful. Please correct the errors and try again.") @using (Html.BeginForm()) { <div> <fieldset> <legend>Or Login Normally</legend> <div class="editor-label"> @Html.LabelFor(m => m.UserName) </div> <div class="editor-field"> @Html.TextBoxFor(m => m.UserName) @Html.ValidationMessageFor(m => m.UserName) </div> <div class="editor-label"> @Html.LabelFor(m => m.Password) </div> <div class="editor-field"> @Html.PasswordFor(m => m.Password) @Html.ValidationMessageFor(m => m.Password) </div> <div class="editor-label"> @Html.CheckBoxFor(m => m.RememberMe) @Html.LabelFor(m => m.RememberMe) </div> <p> <input type="submit" value="Log On" /> </p> </fieldset> </div> }
پس از اجرای پروژه صفحه ای شبیه به پایین مشاهده کرده و سرویس دهنده OpenID خاص خود را میتوانید انتخاب نمایید.
7- برای فعال سازی عملیات احراز هویت توسط FormsAuthentication در سایت باید تنطیمات زیر را در فایل web.config انجام دهید.
<authentication mode="Forms"> <forms loginUrl="~/Account/LogOn" timeout="2880" /> </authentication>
جهت مطالعات بیشتر ودانلود نمونه کدهای آماده میتوانید به لینکهای (^ و ^ و ^ و ^ و ^ و ^ و ^ ) مراجعه کنید.
کد کامل پروژه را میتوانید از اینجا دانلود نمایید.
منبع
در اینجا امکان قرار دادن یک مجموعهی کامل از ردیفها و ستونها، داخل یک ستون از پیش موجود نیز وجود دارد. برای اینکار ابتدا یک row جدید را داخل یک ستون موجود ایجاد میکنیم. با اینکار بلافاصله دسترسی به گرید 12 ستونهی بوت استرپ را داخل آن ستون خواهیم داشت؛ به همراه تمام کلاسهایی که تاکنون آنها را بررسی کردیم.
یک مثال: ایجاد ستونهای تو در تو
<head> <style> img { width: 100%; height: 200px; max-height: 200px; } </style> </head> <body> <div class="container" id="services"> <div class="row"> <section class="col-sm-8"> <img src="images/image.png" alt="sample image"> <h4>Exotic Pets</h4> <p>We offer <strong>specialized</strong> care for <em>reptiles, rodents, birds,</em> and other exotic pets.</p> </section> <section class="col-sm-4"> <div class="row no-gutters"> <div class="col-2 col-sm-4"> <img src="images/image.png" class="img-thumbnail" alt="sample image"> <p>Image 1</p> </div> <div class="col-2 col-sm-4"> <img src="images/image.png" class="img-thumbnail" alt="sample image"> <p>Image 2</p> </div> <div class="col-2 col-sm-4"> <img src="images/image.png" class="img-thumbnail" alt="sample image"> <p>Image 3</p> </div> <div class="col-2 col-sm-4"> <img src="images/image.png" class="img-thumbnail" alt="sample image"> <p>Image 4</p> </div> <div class="col-2 col-sm-4"> <img src="images/image.png" class="img-thumbnail" alt="sample image"> <p>Image 5</p> </div> <div class="col-2 col-sm-4"> <img src="images/image.png" class="img-thumbnail" alt="sample image"> <p>Image 6</p> </div> </div> </section> </div> </div> </body>
و با اندازهی صفحهی بزرگتر از sm:
توضیحات:
تعریف گرید تو در تو را در داخل دومین section تعریف شده، در کدهای فوق مشاهده میکنید:
<body> <div class="container" id="services"> <div class="row"> <section class="col-sm-8"> </section> <section class="col-sm-4"> <div class="row no-gutters"> </div> </section> </div> </div> </body>
به صورت پیشفرض در بین ستونها، یک فاصلهی 15px پیشفرض وجود دارد که به آن Gutter نیز گفته میشود. برای عدم نمایش و اعمال آن میتوان از کلاس no-gutters استفاده کرد. به همین جهت در تصویر دوم، ستونهای تعریف شده به هم چسبیدهاند.
سپس هر ستون داخل این ردیف را به صورت زیر تعریف کردهایم:
<div class="col-2 col-sm-4"> <img src="images/image.png" class="img-thumbnail" alt="sample image"> <p>Image 1</p> </div>
امکان تغییر ترتیب نمایش ستونهای گرید بوت استرپ 4
امکان تغییر ترتیب نمایش ستونهای گرید، در بوت استرپ 4 پیش بینی شدهاست و این مورد نیز بر اساس break-pointهای مختلف، قابل تنظیم است که فرمول کلاسهای آنرا در ذیل مشاهده میکنید:
در اینجا ذکر break-point اختیاری است و عدد ord بین یک تا 12 تغییر میکند.
یک مثال: تغییر ترتیب نمایش ستونهای گرید
<head> <style> img { width: 100%; height: 200px; max-height: 200px; } </style> </head> <body> <div class="container" id="services"> <h2>Flex Order</h2> <div class="row"> <section class="col order-2 d-flex flex-column"> <img src="images/image.png" class="order-2" alt="sample image"> <h4>1. Exotic Pets</h4> <p>We offer <strong>specialized</strong> care for <em>reptiles, rodents, birds,</em> and other exotic pets.</p> </section> <section class="col order-1"> <img src="images/image.png" alt="sample image"> <h4>2. Grooming</h4> <p>Our therapeutic <span class="font-weight-bold">grooming</span> treatments help battle fleas, allergic dermatitis, and other challenging skin conditions.</p> </section> <section class="col order-3"> <img src="images/image.png" alt="sample image"> <h4>3. General Health</h4> <p>Wellness and senior exams, ultrasound, x-ray, and dental cleanings are just a few of our general health services.</p> </section> </div> </div> </body>
در این مثال توسط کلاس order، مکان ستونها را تغییر داده و اولین ستون را در مکان دوم، دومی را در مکان اول و سومی را در همان مکان خودش نمایش دادهایم. باید دقت داشت که در حین تعریف کلاس order بهتر است برای تمام ستونها این ترتیب را تعریف کرد تا با نتایج ناخواستهای مواجه نشویم.
همچنین کلاس order را به سایر المانهای صفحه نیز میتوان اعمال کرد. برای مثال در تصویر فوق، در ستون دوم نمایش داده شده، متن در بالا و تصویر در پایین قرار گرفتهاست. اینکار را با تبدیل این ستون به یک flex column با افزودن کلاسهای d-flex flex-column انجام دادهایم. سپس با اعمال کلاس order-2 به تصویر، این تصویر ذیل متن نمایش داده شدهاست.
یکی از کاربردهای تغییر ترتیب نمایش ستونها در دنیای واقعی، افزودن break-point به آنها (مطابق فرمول یاد شده) و سپس نمایش منوها، پیش از محتویات صفحه در اندازههای کوچکتر صفحه است. برای مثال اگر در حالت عادی، منوهای کنار صفحه نمایش داده میشوند و در ستون سوم قرار گرفتهاند، شاید بخواهید در اندازهی نمایش موبایل، ترتیب نمایش این منوها بالاتر از متن صفحه باشد و در ابتدا قرارگیرد و نه در ترتیب سوم.
امکان تغییر تراز ستونهای گرید بوت استرپ 4
چون طراحی گرید بوت استرپ 4 مبتنی بر Flexbox است، کلاسهای قابل توجهی از آن جهت غنی سازی این سیستم طرحبندی قابل استفاده هستند:
- برای تغییر تراز عمودی ستونها، کلاس align-items-ALN را میتوان به «ردیفها» اعمال کرد. در اینجا ALN یکی از مقادیر start ،center و end را میتواند داشته باشد.
- برای تغییر تراز خود ستونها، کلاس align-self-ALN را میتوان به «ستونها» اعمال کرد. در اینجا نیز ALN یکی از مقادیر start ،center و end را میتواند داشته باشد.
- برای تغییر تراز افقی ستونها، کلاس justify-content-ALN را میتوان به «ردیفها» اعمال کرد. البته ذکر عرض ستونها در این حالت الزامی است. در اینجا ALN یکی از مقادیر start ،center ،around ،between و end را میتواند داشته باشد.
مثال: بررسی روش تغییر تراز ستونها
<head> <style> img { width: 100%; height: 100px; max-height: 100px; } </style> </head> <body> <div class="container" id="services"> <div class="row bg-info align-items-center" style="height: 100vh;"> <div class="col"> <div class="row"> <section class="col"> <img src="images/image.png" alt="sample image"> <h4>Exotic Pets</h4> <p>We offer specialized care for reptiles, rodents, birds, and other exotic pets.</p> </section> <section class="col"> <img src="images/image.png" alt="sample image"> <h4>Grooming</h4> <p>Our therapeutic grooming treatments help battle fleas, allergic dermatitis, and other challenging skin conditions.</p> </section> <section class="col"> <img src="images/image.png" alt="sample image"> <h4>General Health</h4> <p>Wellness and senior exams, ultrasound, x-ray, and dental cleanings.</p> </section> </div> </div> </div> <div class="row bg-success" style="height: 100vh;"> <section class="col"> <img src="images/image.png" alt="sample image"> <h4>Exotic Pets</h4> <p>We offer specialized care for reptiles, rodents, birds, and other exotic pets.</p> </section> <section class="col align-self-center"> <img src="images/image.png" alt="sample image"> <h4>Grooming</h4> <p>Our therapeutic grooming treatments help battle fleas, allergic dermatitis, and other challenging skin conditions.</p> </section> <section class="col align-self-end"> <img src="images/image.png" alt="sample image"> <h4>General Health</h4> <p>Wellness and senior exams, ultrasound, x-ray, and dental cleanings.</p> </section> </div> <div class="row bg-warning justify-content-center" style="height: 100vh;"> <section class="col-4"> <img src="images/image.png" alt="sample image"> <h4>Exotic Pets</h4> <p>We offer specialized care for reptiles, rodents, birds, and other exotic pets.</p> </section> <section class="col-4"> <img src="images/image.png" alt="sample image"> <h4>Grooming</h4> <p>Our therapeutic grooming treatments help battle fleas, allergic dermatitis, and other challenging skin conditions.</p> </section> </div> </div><!-- container --> </body>
در اینجا برای هر ردیف یک height: 100vh درنظر گرفته شدهاست تا کل ارتفاع view-port را پر کند و همچنین برای هر ردیف نیز یک رنگ پس زمینه درنظر گرفتهایم تا تغییر ترازها، مشخصتر باشند.
ابتدا داخل container چنین تعریفی را مشاهده میکنید:
<div class="row bg-info align-items-center" style="height: 100vh;"> <div class="col"> <div class="row"> <section class="col">
وجود row و col بعدی که داخل col اصلی تعریف شدهاست، سبب میشوند تا تمام آیتمها در یک سطر و در یک تراز افقی نمایش داده شوند. اگر این row و col دوم را حذف کنیم، هر آیتم نسبت به محتوای آن در میانهی صفحه قرار میگیرد و یکی بالاتر و دیگری پایینتر نمایش داده خواهند شد.
سپس در ردیف بعدی، کلاسهای align-self-center و align-self-end را بر روی ستونها آزمایش کردهایم:
و در آخر تاثیر اعمال justify-content-center را بر روی یک ردیف مشاهده میکنید:
همانطور که مشاهده میکنید، این کلاسهای Flexbox، کار با ستونهای بوت استرپ را بسیار انعطاف پذیر کردهاند.
روشهای دیگری برای تعیین محل قرارگیری ستونهای بوت استرپ 4
علاوه بر روشهایی که تاکنون آنها را بررسی کردیم، کلاسهای دیگری نیز برای تعیین محل قرارگیری ستونهای بوت استرپ تدارک دیده شدهاند:
- کلاسهای تعیین محل ستونها: fixed-top, fixed-bottom, sticky-top
fixed-top: ستون را در بالای صفحه قرار میدهد.
fixed-bottom: ستون و المان را در پایین صفحه قرار میدهد.
sticky-top: ستون و المان را در بالای صفحه قرار میدهد و با اسکرول صفحه به پایین، باز هم این المان در همان بالای صفحه قابل مشاهدهاست.
- کلاسهای نمایشی برای شبیه سازی ویژگیهای CSS:
این کلاسها با d شروع میشوند؛ به همراه یک break-point اختیاری که هدف آنها در اختیار گذاشتن توانمندیهای نمایشی CSS در بوت استرپ است.
برای مثال کلاس d-md-none به این معنا است که پس از رد شدن از اندازهی md، این المان دیگر نمایش داده نخواهد شد.
- کلاسهای container مقدماتی Flex:
این کلاسها که موارد داخل پرانتز آنها اختیاری است، المان را تبدیل به یک المان Flexbox میکنند. حالت نمایشی پیشفرض آنها block است؛ اما اگر نیاز بود میتوان آنها را تبدیل به in-line نیز کرد.
یک مثال: بررسی روشهای متفاوت تعیین محل قرارگیری المانها
اگر کلاس fixed-bottom را به المانی انتساب دهیم:
<div class="container bg-success"> <div class="bg-info fixed-bottom"> <div class="item">Exotic Pets</div> <div class="item">Grooming</div> <div class="item">Health</div> </div>
کلاس fixed-top نیز چنین کاری را انجام میدهد، فقط المان را بجای پایین صفحه، در بالای صفحه به صورت ثابت نمایش خواهد داد.
در اینجا اگر کلاس sticky-top را اعمال کنیم، هرچند شبیه به fixed-top عمل میکند، اما با container تراز است:
تاثیر کلاسهای flex را در قسمت بعدی به تفصیل بررسی خواهیم کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: Bootstrap4_05.zip
منابع اصلی Ember.js
پیش از شروع به بحث نیاز است با تعدادی از سایتهای اصلی مرتبط با Ember.js آشنا شد:
سایت اصلی: http://emberjs.com
مخزن کدهای آن: https://github.com/emberjs
انجمن اختصاصی پرسش و پاسخ: http://discuss.emberjs.com
موتور قالبهای آن: http://handlebarsjs.com
لیست منابع مطالعاتی مرتبط مانند ویدیوهای آموزشی و لیست مقالات موجود: http://emberwatch.com
و بستهی نیوگت آن: https://www.nuget.org/packages/EmberJS
مفاهیم پایهای Ember.js
شیء Application
App = Ember.Application.create();
مسیر یابی
با مرور قسمتهای مختلف برنامه توسط کاربر، نیاز است حالات برنامه را مدیریت کرد؛ اینجا است که کار قسمت مسیریابی شروع میشود. مسیریابی، منابع مورد نیاز جهت آدرسهای مشخصی را تامین میکند.
App.Router.map(function() { this.resource('accounts'); // takes us to /accounts this.resource('gallery'); // takes us to /gallery });
به این ترتیب مسیرهای accounts/ و gallery/ قابل پردازش خواهند شد.
این مسیرها، تو در تو نیز میتوانند باشند. برای مثال:
App.Router.map(function() { this.resource('news', function() { this.resource('images', function () { // takes us to /news/images this.route('add');// takes us to /news/images/add }); }); });
مدلها
مدلها همان اشیایی هستند که برنامه مورد استفاده قرار میدهد و میتوانند یک آرایهی ساده و یا اشیاء JSON دریافتی از وب سرور باشند.
حداقل به دو روش میتوان مدلها را تعریف کرد:
الف) با استفاده از افزونهی Ember Data
ب) با کمک شیء Ember.Object
App.SiteLink = Ember.Object.extend({}); App.SiteLink.reopenClass({ findAll: function() { var links = []; //… $.getJSON … return links; } });
در ادامه متد دلخواهی را ایجاد کرده و برای مثال آرایهای از اشیاء دلخواه جاوا اسکریپتی را بازگشت خواهیم داد.
پس از تعریف مدل، نیاز است آنرا به سیستم مسیریابی معرفی کرد:
App.GalleryRoute = Ember.Route.extend({ model: function() { return App.SiteLink.findAll(); } });
کنترلرها
کنترلرها جهت ارائهی اطلاعات مدلها به View و قالب برنامه تعریف میشوند. در اینجا همیشه باید بخاطر داشت که model تامین کنندهی اطلاعات است. کنترلر جهت در معرض دید قرار دادن این اطلاعات، به View برنامه کاربرد دارد و مدلها هیچ اطلاعی از وجود کنترلرها ندارند.
کنترلرها علاوه بر اطلاعات model، میتوانند حاوی یک سری خواص و اشیاء صرفا نمایشی که قرار نیست در بانک اطلاعاتی ذخیره شوند نیز باشند.
در Ember.js قالبها (templates) اطلاعات خود را از کنترلر دریافت میکنند. کنترلرها اطلاعات مدل را به همراه سایر خواص نمایشی مورد نیاز در اختیار View و قالبهای برنامه قرار میدهند.
برای تعریف یک کنترلر میتوان درون شیء مسیریابی، با تعریف متد setupController شروع کرد:
App.GalleryRoute = Ember.Route.extend({ setupController: function(controller) { controller.set('content', ['red', 'yellow', 'blue']); } });
روش دوم تعریف کنترلرها با ایجاد یک زیر کلاس از شیء Ember.Controller انجام میشود:
App.GalleryController = Ember.Controller.extend({ search: '', content: ['red', 'yellow', 'blue'], query: function() { var data = this.get('search'); this.transitionToRoute('search', { query: data }); } });
قالبها یا templates
قالبها قسمتهای اصلی رابط کاربری را تشکیل خواهند داد. در اینجا از کتابخانهای به نام handlebars برای تهیه قالبهای سمت کاربر کمک گرفته میشود.
<script type="text/x-handlebars" data-template-name="sayhello"> Hello, <strong>{{firstName}} {{lastName}}</strong>! </script>
<script type="text/x-handlebars" data-template-name="sayhello"> Hello, <strong>{{firstName}} {{lastName}}</strong>! {{#if person}} Welcome back, <strong>{{person.firstName}} {{person.lastName}}</strong>! {{/if}} <ul> {{#each friend in friends}} <li> {{friend.name}} </li> {{/each}} </ul> <img {{bindAttr src="link.url" }} /> {{#linkTo ''about}}About{{/linkTo}} </script>
بهترین مرجع آشنایی با ریز جزئیات کتابخانهی handlebars، مراجعه به سایت اصلی آن است.
قواعد پیش فرض نامگذاری در Ember.js
اگر به مثالهای فوق دقت کرده باشید، خواصی مانند GalleryController و یا GalleryRoute به شیء App اضافه شدهاند. این نوع نامگذاریها در ember.js بر اساس روش convention over configuration کار میکنند. برای نمونه اگر مسیریابی خاصی را به نحو ذیل تعریف کردید:
this.resource('employees');
کنترلر آن App.EmployeesController
مدل آن App.Employee
View آن App.EmployeesView
و قالب آن employees
بهتر است تعریف شوند. به عبارتی اگر اینگونه تعریف شوند، به صورت خودکار توسط Ember.js یافت شده و هر کدام با مسئولیتهای خاص مرتبط با آنها پردازش میشوند و همچنین ارتباطات بین آنها به صورت خودکار برقرار خواهد شد. به این ترتیب برنامه نظم بهتری خواهد یافت. با یک نگاه میتوان قسمتهای مختلف را تشخیص داد و همچنین کدنویسی پردازش و اتصال قسمتهای مختلف برنامه نیز به شدت کاهش مییابد.
تهیهی اولین برنامهی Ember.js
تا اینجا نگاهی مقدماتی داشتیم به اجزای تشکیل دهندهی هستهی Ember.js. در ادامه مثال سادهای را جهت نمایش ساختار ابتدایی یک برنامهی Ember.js، بررسی خواهیم کرد.
بستهی Ember.js را همانطور که در قسمت منابع اصلی آن در ابتدای بحث عنوان شد، میتوانید از سایت و یا مخزن کد آن دریافت کنید و یا اگر از VS.NET استفاده میکنید، تنها کافی است دستور ذیل را صادر نمائید:
PM> Install-Package EmberJS
در این حالت ترتیب تعریف اسکریپتهای مورد نیاز صفحه به صورت ذیل خواهند بود:
<script src="Scripts/jquery-2.1.1.js" type="text/javascript"></script> <script src="Scripts/handlebars.js" type="text/javascript"></script> <script src="Scripts/ember.js" type="text/javascript"></script> <script src="Scripts/app.js" type="text/javascript"></script>
App = Ember.Application.create(); App.IndexRoute = Ember.Route.extend({ setupController:function(controller) { controller.set('content', ['red', 'yellow', 'blue']); } });
App.Router.map(function() { this.resource('application'); this.resource('index'); });
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> <script src="Scripts/jquery-2.1.1.js" type="text/javascript"></script> <script src="Scripts/handlebars.js" type="text/javascript"></script> <script src="Scripts/ember.js" type="text/javascript"></script> <script src="Scripts/app.js" type="text/javascript"></script> </head> <body> <script type="text/x-handlebars" data-template-name="index"> Hello, <strong>Welcome to Ember.js</strong>! <ul> {{#each item in content}} <li> {{item}} </li> {{/each}} </ul> </script> </body> </html>
مقدار data-template-name در اینجا مهم است. اگر آنرا به هر نام دیگری بجز index تنظیم کنید، منبع دریافت اطلاعات آن مشخص نخواهد بود. نام index در اینجا به معنای اتصال این قالب به اطلاعات ارائه شده توسط کنترلر index است.
تا همینجا اگر برنامه را اجرا کنید، به خوبی کار خواهد کرد. نکتهی دیگری که در مورد قالبهای Ember.js قابل توجه هستند، قالب پیش فرض application است. با تعریف Ember.Application.create یک چنین قالبی نیز به ابتدای هر صفحه به صورت خودکار اضافه خواهد شد:
<body> <script type="text/x-handlebars" data-template-name="application"> <h1>Header</h1> {{outlet}} </script>
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
EmberJS01.zip
پیش نیاز ورود به دنیای داده کاوی
علم داده کاوی از علوم مختلفی از جمله علم آمار، هوش مصنوعی، یادگیری ماشین، شناسائی الگو و پایگاه داده نشات گرفته است و این علوم ریشههای علم داده کاوی هستند. برای مثال الگوریتم هایی که یک مدل را یاد میگیرند یا الگویی را شناسائی میکنند؛ معمولا وجه مشترک یادگیری ماشین و شناسائی الگو با داده کاوی هستند.
در این قسمت پیش از درگیر شدن با جزئیات هر الگوریتم تمایل دارم خوانندگان محترم را با مطالبی که شاید کمتر در دنیای IT با آن درگیر بوده اند؛ آشنا کنم. این کار به این دلیل انجام میشود که برای مثال در کشف قوانین انجمنی یا دسته بند مبتنی بر قانون (مثال متداول آن تحلیل سبد خرید مشتری در هایپر مارکت است) خروجی به شکل مجموعه ای قانون «اگر الف؛ آنگاه ب» و ... بدست میآید. بنابراین برای تفسیر صحیح این مدلها علاوه بر آشنائی با کسب وکار مربوطه؛ نیازی نسبی به آشنائی با سایر علوم نیز میباشد و بدین ترتیب از اتلاف انرژی و زمان و همچنین از بروز خطا در استدلالمان جلوگیری میکنیم. جمله معروفی با این مضمون در سایر فرهنگها وجود دارد که اعداد دروغ نمیگویند؛ ولی فردی دروغگو میتواند از اعداد سوء استفاده کند. بنابراین زمان مناسبی است که با بعضی مغالطات آشنا شویم.
اساس کار علمی به بیان ساده عبارت است از: به پرسش گرفتن همه چیز و دنبال کردن مدارک و شواهد به هر کجا که ما را رهنمون سازد؛ اینکار بوسیله آزمودن هر نظر و ایده ای، با انجام آزمایش روی آنها و مشاهده نتایج بدست آمده و سپس توسعه دادن مواردی که از آزمایشات موفق بیرون آمده اند و رد کردن آنهایی که در آزمون شکست خورده اند، انجام میگیرد. روش علمی آنچنان قدرتمند است که در طی چهار قرن گذشته (قرن 16 میلادی) ما را از نخستین نگاهی که گالیله از درون تلسکوپ به دنیای دیگر انداخت، به گام گذاشتن بر روی ماه رسانده است و به ما اجازه داده تا به پهنه فضا و زمان بنگریم تا کشف کنیم که در کجا و در چه زمانی از عالم قرار داریم.
اجداد ما ستاره شناسان خانه به دوشی بودند که در گروههای کوچک زندگی میکردند، آسمان تقویم و راهنمای زندگی آنها بود، بقای شان به این وابسته بود که بدانند چگونه ستارهها را بخوانند و بدین ترتیب بتوانند فرا رسیدن زمستان را پیش بینی کنند و زمان کوچ کردن را بدست آورند. در واقع نعمت تشخیص الگو باعث شانس بیشتر زنده ماندن و تولید مثل آنها بود و بدین ترتیب ژنهای تشخیص الگو را به نسلهای آینده منتقل میکردند. آنها وقتی که ارتباط مستقیمی بین حرکت ستارگان و گردش فصلی حیات روی زمین پیدا کردند، نتیجه گرفتند که اتفاقاتی که آن بالا میافتد به ما در پائین مربوط میشود و آنرا به خود میگرفتند!؟ آنها توضیح منطقی دیگری برای اتفاق پیش آمده نداشتند. کلمه یونانی Dis-aster به معنی "ستاره شوم" حتی برای اقوام مختلف به معنای جنگ، قحطی، مریضی و ... تعبیر میشد. (در فرهنگ ما نیز جملاتی با این مضمون کم وجود ندارد، برای مثال:" قمر در عقرب است"، پس اتفاق بدی خواهد افتاد!. البته منظور قرار گرفتن ماه در برج عقرب است و ...).
می توان گفت استعداد انسان در تشخیص الگو شمشیری دو لبه است، ما انسانها قادریم در تشخیص الگوهائی که اصلاً وجود ندارند نیز خیلی خوب عمل کنیم!، چیزی که به معنای "تشخیص الگوی اشتباه" است. ما عاشق خاص بودن هستیم و با داشتن این هدف همواره در تلاش برای فریب خود و دیگران هستیم. علم در مرز میان دانایی و جهالت گام بر میدارد، از نظر یک محقق هیچ شرمساری در ندانستن وجود ندارد، تنها شرمساری در آن است که تظاهر کنیم همه جوابها را میدانیم. علم راهی است که انسان را از فریب خود و دیگران باز میدارد و امروزه به نیکی میدانیم هر چه علم بیشتر در اختیار ابنای بشر قرار گیرد، امکان سوء استفاده از آن کمتر خواهد شد. بدین ترتیب با دانستن ارزشهای علمی تقاضا برای جهالت و تعصب کم خواهد شد. ارزشهای علمی مختصراً به شرح زیر هستند: قدرت سوال کردن، وقتی موضوعی را بررسی میکنید تنها چیزی که باید از خودتان بپرسید این است که واقعیتها در این موضوع (فلسفه) چه هست و چه حقایقی در آن نهفته است. هیچگاه به خودتان اجازه ندهید که آنچه را دوست دارید، حقیقت داشته باشد (اگر یک ایده دلخواه در یک آزمایش خوب مردود شد، پس اشتباه است و از آن عبور کنید)، همچنین آنچه را که فکر میکنید حقیقت بودنش برای بشر سودمند است شما را منحرف نکند (برای خودتان فکر کنید و از خودتان بپرسید)، فقط و تنها به این که واقعیت چه هست بنگرید، در ضمن اگر مدرکی ندارید؛ قضاوت نکنید و مهمترین قانون؛ به یاد داشته باشید که شما انسان هستید و میتوانید اشتباه کنید، همانطور که مهمترین دانشمندان در مواردی اشتباهاتی داشته اند.
منطق ابزاری علمی است که بکارگیری آن ذهن انسان را از خطای در تفکر باز میدارد، مبارزه با مغالطات و لغزشهای اندیشه هدف علم منطق است. مغالطه منحصر به استدلال نیست، به بیان دقیقتر شکل هایی از استدلال است که نتیجه تابع مقدمه یا مقدمه هایش نیست. مغالطه ای که عمدی یعنی با آگاهی از عدم اعتبار انجام میشود اما به ظاهر معتبر و مجاب کننده و در واقع فریب دهنده مخاطب است سفسطه نامیده میشود. عدم اعتبار یک استدلال ممکن است به دلایل زیر باشد: ناشی از نادرستی یکی از مقدمات استدلال باشد و یا علی رغم درستی مقدمات؛ نظم و صورت استدلال نادرست باشد. برای آشنایی ذهن خواننده به معرفی نمونه ای از این مغالطات اشاره میشود؛ برای مثال این مغالطه بر این پیش فرض استوار است که هر زمان دو حادثه با یکدیگر اتفاق افتاد؛ میتوان یکی را علت و دیگری را معلول آن به حساب آورد. برای مثال در تحقیقی به ارتباط مستقیم میان وجود داشتن چتر در ماشین به هنگام تصادفات رانندگی پرداخته شده و به این نتیجه رسیده اند زمانی که تصادفی رخ میدهد با احتمال بسیار بالاتری چتر در ماشین وجود دارد به نسبت حالتی که چتر در ماشین وجود ندارد؛ به همین دلیل چتر عامل تصادف است! برای اجتناب از این مغالطات باید قادر به تفکیک اصل علیت (Causality) و همبستگی (Correlation) باشیم. (در توضیح مثال فوق لغزندگی جاده عامل تصادف در روزی بارانی است نه چتر!).
همچنین استفاده از آمار و اطلاعات آماری علی رغم فوائد زیاد در اطلاع رسانی، میتواند لغزشگاهی باشد که زمینه ارتکاب برخی مغالطات را نیز فراهم کند در ادامه به معرفی تعدادی از این مغالطات آماری (Statistical Fallacies) میپردازیم:
مغالطه متوسط که میتواند با سوء استفاده از برخی اصطلاحات آماری مطابق با اهداف و اغراضی که موسسات ارائه دهنده اطلاعات آماری دنبال میکنند، متوسط یک مجموعه را کم یا زیاد اعلام کنند! به بیان دیگر کلمه متوسط در نوبتهای مختلف به معانی متداولی استعمال میشود که عبارتند از:
میانگین (Average) یا معدل که برای چند عدد برابر است با مجموع آنها تقسیم بر تعدادشان.
میانه (Median) که یک مجموعه عددی را به دو نیم تقسیم میکند؛ نیمی که هر یک از اعداد آن بیشتر از میانه و نیمی که کمتر از میانه است.
نما (Mode) که در یک مجموعه؛ عددی است که بیش از دیگر اعداد تکرار شده است.
پس میتوان نتیجه گرفت وقتی اعلام میشود که در یک جامعه آماری فلان عدد یک متوسط است هنوز اطلاع دقیقی داده نشده و باید صراحتا مشخص کنند کدامیک از معانی متوسط مورد نظر است.
باید در نظر داشته باشید این مغالطه زمانی استفاده میشود که دامنه تغییرات در میان جامعه آماری بسیار زیاد است، چنانچه دامنه تغییرات حداقل و حداکثر نسبت به تعداد افراد جامعه زیاد نباشد، مقادیر میانگین؛ میانه و نما تقریبا منطبق بر هم خواهند شد (برای مثال در محاسبه متوسط طول قد افراد یک کشور). اما در مواردی که تغییرات مذکور زیاد باشد باید با هوشیاری از وقوع این مغالطه جلوگیری نمود (از مصادیق و زمینههای بارز و مهم ارتکاب این مغالطه محاسبه متوسط حقوق و درآمد افراد است).
مغالطه نمودارهای گمراه کننده (Misleading Graph) استفاده از نمودار میتواند وسیله ای موثر در بیان مغالطه آمیز بودن اطلاعات آماری باشد. برای مثال نمودار رشد سود خالص شرکتی را در نظر بگیرید که در محور افقی آن بعد زمان و در محور عمودی مقادیر مالی درج شده است. با رسم نمودار مذکور سود خالص هر ماه به صورت واضح و آشکار مثلاْ رشدی ده درصدی را نمایش میدهد چنانچه شرکت مذکور اصول اخلاقی را رعایت نکند و برای جذابیت بیشتر و جذب سرمایههای بیشتر؛ قسمت هایی از نمودار را به گونه ای حذف کند که حاصل کار این شود که خواننده احساس کند سود خالص شرکت در عرض دوازده ماه به بالای کاغذ رسیده (یعنی به طور ضمنی افزایشی معادل صد در صد) و یا نسبت بین خطوط افقی و عمودی را بگونه ای تغییر دهد تا رشد ده درصدی را بسیار بزرگتر نشان داده شود (می تواند با تقلیل مقیاس واحد مالی به یک دهم به این هدف برسد) بدین ترتیب نمودار حاصل چنان جذاب میشود که هر کس با تماشای آن رگههای موفقیت و پیشرفت را در شرکت متقلب بوضوح مشاهده میکند.
مغالطه تصاویر یک بعدی (One Dimensional Pictures) از روشهای تقلب دیگر میتواند باشد که باید توجه کرد آیا نسبت القا شده بوسیله تصاویر با نسبت اعداد مطابقت دارد یا خیر.
می دانیم آنچه پایه و اساس آمار استنباطی را تشکیل میدهد روشهای نمونه گیری است که اتفاقاْ این روشها منشاء برخی مغالطات و ترفندهای آماری نیز هست در این قسمت به معرفی تعدادی از این موارد میپردازیم:
نمونه ناکافی (Deficient Examples) چنانچه در روش نمونه گیری مقدار و نسبت «نمونه» به «جامعه آماری» به اندازه کافی بزرگ باشد و به طرز صحیحی انتخاب شده باشد؛ غالبا میتواند معرف خوبی برای جامعه آماری باشد. اما چنانچه نمونه به اندازه کافی بزرگ نباشد؛ گرچه اطلاعاتی را در خصوص جامعه آماری در اختیارمان قرار میدهد ولیکن احتمال وقوع خطا در چنین حالتی بسیار زیاد است که این مغالطه دارای این شرایط است؛ البته باید توجه داشت که کافی یا ناکافی بودن تعداد نمونهها نسبت به جامعه آماری امری نسبی است. بنابراین جهت اجتناب از بروز این مغالطه باید همواره در نظر داشت آیا تعداد نمونهها در مقایسه با کل جامعه آماری راضی کننده و کافی است یا خیر.
نمونه غیر تصادفی (Deliberate Examples) برای بدست آوردن اطلاعات آماری در روش نمونه برداری؛ کافی بودن نمونهها شرط لازم است و کافی نیست؛ یکی از مواردی که باید مورد توجه قرار داد تصادفی بودن نمونهها میباشد. به بیان دیگر تنها کافی بودن نمونهها یا فراوانی آنها برای تعمیم دادن حکمی به کل آن جامعه آماری کفایت نمیکند. تصادفی بودن نمونهها بدین معناست که نمونهها نباید نماینده و بیانگر دسته و گروه خاصی از جامعه آماری باشند. همچنین در روش نمونه برداری افراد جامعه آماری باید از شانس یکسانی برای انتخاب شدن در نمونه برداری برخوردار باشند از راههای تحقق این هدف تقسیم افراد جامعه آماری به دستهها و طبقات مختلف و تعیین کردن درصد و نسبت هر یک از آنها به کل مجموعه میباشد بدین ترتیب در نمونه برداری نیز سعی میشود این نسبت لحاظ گردد؛ این روش اصطلاحا روش نمونه گیری تصادفی طبقه ای نامیده میشود روشهای دیگری نیز به منظور اینکه کلیه افراد جامعه آماری از شانسی یکسان برای انتخاب شدن در نمونه برخوردار باشند وجود دارد مانند روشهای نمونه گیری تصادفی ساده؛ نمونه گیری تصادفی خوشه ای و نمونه گیری تصادفی سیستماتیک.
عدم واقع نمائی نمونهها (Unrealistic Examples ) در نمونه برداری به صورت پرسشهای شفاهی از جامعه آماری انسانی مسئله عدم واقع نمائی نمونهها رخ میدهد بدین ترتیب همواره موجب بروز خطاهای جدی در بدست آوردن اطلاعات آماری دقیق است. این مشکل عملا به روش جمع آوری دادهها از طریق مصاحبه بر میگردد خواه به صورت نمونه ای یا سرشماری باشد.