ASP.NET MVC #8
جواب دادند که در ورژن بعدی، فعلا پیاده سازی برای این موضوع انجام نشده و به جای آن فیوچری به نام Tag Helpersدر ورژن بعدی ارائه خواهد شد.
پر نکردن فیلدهای Textbox فایل PDF
ASP.NET MVC #22
روشهایی که قرار هست در ادامه توضیح داده شوند بر اساس کوئری بازگشتی میباشند. الگوریتمهای متنوعی بر اساس recursive CTE برای حل این مساله خلق شده اند. که من تنها به دو روش آن اکتفا میکنم.
Recursive CTE در نسخهی 2005 به SQL Server اضافه شده است. توسط این تکنیک مسائل پیچیده و گوناگونی را میتوان بسادگی حل نمود. مخصوصا مسائلی که ماهیت بازگشتی دارند مثل پیمایش یک درخت یا پیمایش یک گراف وزن دار.
روش اول:
یک کوئری بازگشتی دارای دو بخش هست به نامهای Anchor و recursive. در بخش دوم کوئری باز خودش را فراخوانی میکند تا به داده هایی که در مرحله قبل تولید شده اند دسترسی پیدا کند در اولین فراخوانی توسط عضو recursive، دادههای تولید شده در قسمت Anchor قابل دسترسی هستند. در قسمت دوم، کوئری آنقدر خود را فراخوانی میکند تا دیگر سطری از مرحله قبل وجود نداشته باشد که به آن مراجعه کند.
توضیح تکنیک:
در گام اول اندیس شروع و پایان کلمه اول را بدست میآوریم.
سپس در گام بعدی از اندیس پایان کلمه قبلی به عنوان اندیس شروع کلمه جدید استفاده میکنیم.
و اندیس پایان کلمه توسط تابع charindex بدست میآید.
کوئری تا زمانی ادامه پیدا میکند که کلمه برای تجزیه کردن در رشته باقی مانده باشد. فقط فراموش نکنید که حتما باید آخر عبارت یک کارکتر space داشته باشید.
DECLARE @S VARCHAR(50)='I am a student I go to school '; WITH CTE AS ( SELECT 1 rnk, 1 start, CHARINDEX(' ', @s) - 1 ed UNION ALL SELECT rnk + 1, ed + 2, CHARINDEX(' ', @s, ed + 2) - 1 FROM CTE WHERE CHARINDEX(' ', @s, ed + 2) > 0 ) SELECT rnk, SUBSTRING(@s, start, ed - start + 1) AS word FROM CTE /* Result rnk word ----------- ------- 1 I 2 am 3 a 4 student 5 I 6 go 7 to 8 school */
روش دوم:
در این روش در همان CTE عبارت تجزیه میشود و عمل تفکیک به مرحله بعدی واگذار نمیشود،
در گام اول، اولین کلمه انتخاب میشود. و سپس آن کلمه از رشته حذف میشود. با این روش همیشه اندیس شروع کلمه برابر با 1 خواهد بود و اندیس پایان کلمه توسط تابع charindex بدست خواهد آمد.
در گام بعدی اولین کلمه موجود در رشته ای که قبلا اولین کلمه از آن جدا شده است بدست میآید و باز مثل قبلی کلمه انتخاب شده از رشته جدا شده و رشته برش یافته به مرحله بعد منتقل میشود.
در این روش مثل روش قبلی آخر عبارتی که قرار هست تجزیه شود باید یک کارکتر خالی وجود داشته باشد.
DECLARE @a VARCHAR(50)='I am a student I go to school '; WITH MyWords(ranking, word, string) AS( SELECT 1, CAST(SUBSTRING(@a, 1, CHARINDEX(' ', @a) - 1) AS VARCHAR(25)), STUFF(@a, 1, CHARINDEX(' ', @a), '') UNION ALL SELECT ranking + 1, CAST(SUBSTRING(string, 1, CHARINDEX(' ', string) - 1) AS VARCHAR(25)), STUFF(string, 1, CHARINDEX(' ', string), '') FROM MyWords WHERE CHARINDEX(' ', string) > 0 ) SELECT ranking, word FROM MyWords;
ranking word ----------- ------------------------- 1 I 2 am 3 a 4 student 5 I 6 go 7 to 8 school
PS /> $env:PSModulePath -Split ":" /Users/sirwanafifi/.local/share/powershell/Modules /usr/local/share/powershell/Modules /usr/local/microsoft/powershell/7/Modules
PS /> Import-Module ./PingModule.psm1
PS /> Get-Module PingModule ModuleType Version PreRelease Name ExportedCommands ---------- ------- ---------- ---- ---------------- Script 0.0 PingModule Get-PingReply
PS /> Import-Module ./PingModule.ps1 PS /> Get-Module PingModule ModuleType Version PreRelease Name ExportedCommands ---------- ------- ---------- ---- ---------------- Script 0.0 PingModule
Function Get-PingReply { // as before } Function Get-PrivateFunction { Write-Debug 'This is a private function' } Export-ModuleMember -Function @( 'Get-PingReply' )
$moduleSettings = @{ Path = './PingModule.psd1' Description = 'A module to ping a remote system' RootModule = 'PingModule.psm1' ModuleVersion = '1.0.0' FunctionsToExport = @( 'Get-PingReply' ) PowerShellVersion = '5.1' CompatiblePSEditions = @( 'Core' 'Desktop' ) } New-ModuleManifest @moduleSettings
$moduleSettings = @{ Author = 'John Doe' Description = 'This is a sample module' } New-ModuleManifest $moduleSettings
New-ModuleManifest : A parameter cannot be found that matches parameter name 'Author'.
# # Module manifest for module 'PingModule' # # Generated by: sirwanafifi # # Generated on: 01/01/2023 # @{ # Script module or binary module file associated with this manifest. RootModule = './PingModule.psm1' # Version number of this module. ModuleVersion = '1.0.0' # Supported PSEditions CompatiblePSEditions = 'Core', 'Desktop' # ID used to uniquely identify this module GUID = '3f8561fc-c004-4c8e-b2fc-4a4191504131' # Author of this module Author = 'sirwanafifi' # Company or vendor of this module CompanyName = 'Unknown' # Copyright statement for this module Copyright = '(c) sirwanafifi. All rights reserved.' # Description of the functionality provided by this module Description = 'A module to ping a remote system' # Minimum version of the PowerShell engine required by this module PowerShellVersion = '5.1' # Name of the PowerShell host required by this module # PowerShellHostName = '' # Minimum version of the PowerShell host required by this module # PowerShellHostVersion = '' # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. # DotNetFrameworkVersion = '' # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. # ClrVersion = '' # Processor architecture (None, X86, Amd64) required by this module # ProcessorArchitecture = '' # Modules that must be imported into the global environment prior to importing this module # RequiredModules = @() # Assemblies that must be loaded prior to importing this module # RequiredAssemblies = @() # Script files (.ps1) that are run in the caller's environment prior to importing this module. # ScriptsToProcess = @() # Type files (.ps1xml) to be loaded when importing this module # TypesToProcess = @() # Format files (.ps1xml) to be loaded when importing this module # FormatsToProcess = @() # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess # NestedModules = @() # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. FunctionsToExport = 'Get-PingReply' # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = '*' # Variables to export from this module VariablesToExport = '*' # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. AliasesToExport = '*' # DSC resources to export from this module # DscResourcesToExport = @() # List of all modules packaged with this module # ModuleList = @() # List of all files packaged with this module # FileList = @() # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. PrivateData = @{ PSData = @{ # Tags applied to this module. These help with module discovery in online galleries. # Tags = @() # A URL to the license for this module. # LicenseUri = '' # A URL to the main website for this project. # ProjectUri = '' # A URL to an icon representing this module. # IconUri = '' # ReleaseNotes of this module # ReleaseNotes = '' # Prerelease string of this module # Prerelease = '' # Flag to indicate whether the module requires explicit user acceptance for install/update/save # RequireLicenseAcceptance = $false # External dependent modules of this module # ExternalModuleDependencies = @() } # End of PSData hashtable } # End of PrivateData hashtable # HelpInfo URI of this module # HelpInfoURI = '' # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. # DefaultCommandPrefix = '' }
PS /> Test-ModuleManifest ./PingModule.psd1 ModuleType Version PreRelease Name ExportedCommands ---------- ------- ---------- ---- ---------------- Script 1.0.0 PingModule Get-PingReply
PS /> Register-PSRepository -Name 'PSLocal' ` >> -SourceLocation "$(Resolve-Path $RepoPath)" ` >> -PublishLocation "$(Resolve-Path $RepoPath)" ` >> -InstallationPolicy 'Trusted'
ProjectRoot | -- PingModule | -- PingModule.psd1 | -- PingModule.psm1
PS /> Publish-Module -Path ./PingModule/ -Repository PSLocal
PS /> Find-Module -Name PingModule -Repository PSLocal Version Name Repository Description ------- ---- ---------- ----------- 0.0.1 PingModule PSLocal Get-PingReply is a.
PS /> Install-Module -Name PingModule -Repository PSLocal -Scope CurrentUser
ProjectRoot | -- PingModule | -- 1.0.0 | -- PingModule.psd1 | -- PingModule.psm1 | -- 1.0.1 | -- PingModule.psd1 | -- PingModule.psm1
PS /> Publish-Module -Path ./PingModule/1.0.0 -Repository PSLocal PS /> Publish-Module -Path ./PingModule/1.0.1 -Repository PSLocal
PS /> Install-Module -Name PingModule -Repository PSLocal -Scope CurrentUser PS /> Get-InstalledModule -Name PingModule Version Name Repository Description ------- ---- ---------- ----------- 1.0.1 PingModule PSLocal Get-PingReply is a.
ProjectRoot | -- PingModule | -- 1.0.0 | -- Public/ | -- Private/ | -- PingModule.psd1 | -- PingModule.psm1
$ScriptList = Get-ChildItem -Path $PSScriptRoot/Public/*.ps1 -Filter *.ps1 foreach ($Script in $ScriptList) { . $Script.FullName } $ScriptList = Get-ChildItem -Path $PSScriptRoot/Private/*.ps1 -Filter *.ps1 foreach ($Script in $ScriptList) { . $Script.FullName }
PS /> Set-PDFSingature -PdfToSign "./sample_invoice.pdf" -SignatureImage "./sample_signature.jpg"
ProjectRoot | -- SignPdf | -- 1.0.0 | -- Public/ | -- dependencies/ | -- BouncyCastle.Crypto.dll | -- System.Drawing.Common.dll | -- Microsoft.Win32.SystemEvents.dll | -- iTextSharp.LGPLv2.Core.dll | -- Set-PDFSingature.ps1 | -- SignPdf.psd1 | -- SignPdf.psm1
$ScriptList = Get-ChildItem -Path $PSScriptRoot/Public/*.ps1 -Filter *.ps1 foreach ($Script in $ScriptList) { . $Script.FullName } Export-ModuleMember -Function Set-PDFSingature
using namespace iTextSharp.text using namespace iTextSharp.text.pdf using namespace System.IO Function Set-PDFSingature { [CmdletBinding()] Param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [ValidateScript({ if (Test-Path ([Path]::Join($(Get-Location), $_))) { return $true } else { throw "Signature image not found" } if ($_.EndsWith('.pdf')) { return $true } else { throw "File extension must be .pdf" } })] [string]$PdfToSign, [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [ValidateScript({ if (Test-Path ([Path]::Join($(Get-Location), $_))) { return $true } else { throw "Signature image not found" } if ($_.EndsWith('.png') -or $_.EndsWith('.jpg')) { return $true } else { throw "File extension must be .png or .jpg" } })] [string]$SignatureImage, [Parameter(Mandatory = $false, ValueFromPipeline = $true)] [int]$XPos = 130, [Parameter(Mandatory = $false, ValueFromPipeline = $true)] [int]$YPos = 50 ) Try { Add-Type -Path "$PSScriptRoot/dependencies/*.dll" $pdf = [PdfReader]::new("$(Get-Location)/$PdfToSign") $fs = [FileStream]::new("$(Get-Location)/$PdfToSign-signed.pdf", [FileMode]::Create) $stamper = [PdfStamper]::new($pdf, $fs) $stamper.AcroFields.AddSubstitutionFont([BaseFont]::CreateFont()) $content = $stamper.GetOverContent(1) $width = $pdf.GetPageSize(1).Width $image = [Image]::GetInstance("$(Get-Location)/$SignatureImage") $image.SetAbsolutePosition($width - $XPos, $YPos) $image.ScaleAbsolute(100, 30) $content.AddImage($image) $stamper.Close() $pdf.Close() $fs.Dispose() } Catch { Write-Host "Error: $($_.Exception.Message)" } }
PS /> Publish-Module -Path ./SignPdf/1.0.0 -Repository PSLocal
PS /> Install-Module -Name SignPdf -Repository PSLocal -Scope CurrentUser
PS /> Set-PDFSingature -PdfToSign "./sample_invoice.pdf" -SignatureImage "./sample_signature.jpg"
کدهای ماژول را میتوانید از اینجا دانلود کنید.
<Import Project="$(MSBuildExtensionsPath)\Microsoft\MicrosoftAjax\ajaxmin.tasks" /> <Target Name="AfterBuild"> <ItemGroup> <JS Include="**\*.js" Exclude="**\*.min.js;Scripts\*.js" /> </ItemGroup> <ItemGroup> <CSS Include="**\*.css" Exclude="**\*.min.css" /> </ItemGroup> <AjaxMin JsSourceFiles="@(JS)" JsSourceExtensionPattern="\.js$" JsTargetExtension=".min.js" CssSourceFiles="@(CSS)" CssSourceExtensionPattern="\.css$" CssTargetExtension=".min.css" /> </Target>
الف) تهیه فایل nuspec
NuGet قابلیت پذیرش متغیرهای خود تکمیل شوندهای را نیز دارد. فایل nuspec یا جزئیات بستهی تولیدی نیوگت، در این حالت یک چنین شکلی را پیدا میکند:
<?xml version="1.0"?> <package > <metadata> <id>$id$</id> <version>$version$</version> <title>$title$</title> <authors>$author$</authors> <owners>$author$</owners> <licenseUrl>https://site.com/prj/LICENSE</licenseUrl> <projectUrl>https://site.com/prj</projectUrl> <requireLicenseAcceptance>false</requireLicenseAcceptance> <description>$description$</description> <copyright>Copyright 2015 My Name</copyright> </metadata> </package>
بنابراین تکمیل اطلاعات فایل AssemblyInfo.cs را فراموش نکنید. برای مثال اطلاعات AssemblyCompany آن، در قسمت authors فایل فوق جایگزین میشود.
ب) تهیه فایل NuGet.exe
فایل NuGet.exe را جهت کامپایل فایل فرضی TestNuget.nuspec نیاز داریم. میتوان آنرا از سایت کدپلکس تهیه کرد و یا تنها کافی است بر روی Solution موجود در VS.NET کلیک راست کرده و گزینهی Enable NuGet Package Restrore را انتخاب کنیم. با اینکار به صورت خودکار پوشهی ویژهای به نام .nuget به همراه فایل NuGet.exe ایجاد میشود.
ج) کامپایل فایل nuspec
در همان پوشهی جدید .nuget، یک فایل bat را با محتوای ذیل تهیه کنید:
"%~dp0NuGet.exe" pack "..\TestNuGet\TestNuGet.csproj" -Prop Configuration=Release copy "%~dp0*.nupkg" "%localappdata%\NuGet\Cache" pause
در ادامه برای اینکه امکان آزمایش محلی این بسته را نیز داشته باشیم، فایل بستهی تولیدی، به کش محلی نیوگت، در سیستم جاری کپی میشود.
نکتهی جالب این روش، تهیهی قسمت dependencies پروژه، به صورت خودکار است:
<?xml version="1.0"?> <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd"> <metadata> <id>TestNuget</id> <version>1.0.0.0</version> <title>TestNuget</title> <authors>Vahid N.</authors> <owners>Vahid N.</owners> <licenseUrl>https://site.com/prj/LICENSE</licenseUrl> <projectUrl>https://site.com/prj</projectUrl> <requireLicenseAcceptance>false</requireLicenseAcceptance> <description>This is a test.</description> <copyright>Copyright 2015 My Name</copyright> <dependencies> <dependency id="EntityFramework" version="6.1.2" /> </dependencies> </metadata> </package>
د) آزمایش محلی بستهی جدید
اکنون که فایل TestNuget.1.0.0.0.nupkg تولیدی را در آدرس localappdata%\NuGet\Cache% نیز کپی کردهایم، به منوی Tools/Options مراجعه و در قسمت NuGet Package Manager آن، به قسمت Package sources، این آدرس کش محلی را نیز معرفی کنید:
به این ترتیب، بدون ارسال یک بسته به سایت اصلی نیوگت، میتوان بسته را از کش محلی نیوگت نصب و آزمایش کرد:
فایلهای ذکر شده در مطلب فوق را از اینجا میتوانید دریافت کنید:
TestNuget.zip
new Vue({ el: '#app', template: '<div>Hello DNT</div>' });
new Vue({ el: '#app', data() { return { blogTitle: 'DNT' } }, render: function (createElement) { return createElement('h1', this.blogTitle) } });
Vue.component('child', { template: '<div>Hello DNT users</div>' });
Vue.component('child', { props: ['text'], template: `<div> {{ text }} </div>` }); new Vue({ el: '#app', data() { return { message: 'Hello DNT!' } } });
<child :text="message"></child>
Vue.component('blogPost', { props: { post: { type: Object, required: true } }, template: `<div> <h1>{{ post.title }}</h1> <p>{{ post.body }}</p> </div>` });
[Vue warn]: Invalid prop: type check failed for prop "post". Expected Object, got String. found in ---> <BlogPost> <Root>
Vue.component('blogPost', { props: { post: { type: Object, required: true, validator: obj => { const titleIsValid = typeof obj.title === 'string'; const bodyIsValid = typeof obj.body === 'string'; const isValid = titleIsValid && bodyIsValid; if (!isValid) { console.warn("prop is not valid"); return false; } return true; } } }, template: `<div> <h1>{{ post.title }}</h1> <p>{{ post.body }}</p> </div>` });
Vue.component('blogPost', { props: { post: { type: Object, validator: obj => { const titleIsValid = typeof obj.title === 'string'; const bodyIsValid = typeof obj.body === 'string'; const isValid = titleIsValid && bodyIsValid; if (!isValid) { console.warn("prop is not valid"); return false; } return true; }, default: function() { return { title: 'Vue is fun!', body: 'Vue is fun..................' } } } }, template: `<div> <h1>{{ post.title }}</h1> <p>{{ post.body }}</p> </div>` });
data: function () { return { stars: 5, hover: 5 } },
var dt = { stars: 5, hover: 5 }; Vue.component('blogPost', { data: function() { return dt; }, props: // as before..., template: `<div class="blog-post"> <h1>{{ post.title }}</h1> <p>{{ post.body }}</p> <div class="star-wrap"> <span v-for="n in 5" class="star" :class="{ full: hover >= n+1 }" @click="stars = n+1" @mouseover="hover = n+1" @mouseout="hover = stars" ></span> </div> </div>` });
برای رفع این مشکل کافی است به اینصورت دیتا را تعریف کنیم:
Vue.component('blogPost', { data: function() { return { stars: 5, hover: 5 } }, props: // as before..., template: // as before });
تغییر دیتا درون کامپوننتها
تا اینجا توانستیم از کامپوننت والد دادههایی را به کامپوننتهای فرزند ارسال کنیم. اکنون میخواهیم قابلیت تغییر دیتای تعریف شدهی درون کامپوننت والد را درون کامپوننتها نیز داشته باشیم:
Vue.component('child', { props: ['message'], methods: { changeName() { this.message = "New Name!..." } }, template: '#child-template' }); new Vue({ el: '#app', data() { return { name: 'DNT!' } } });
تمپلیت کامپوننت فوق نیز به صورت x-template درون DOM تعریف شده است:
<script type="text/x-template" id="child-template"> <div> <p>{{ message }}</p> <button @click="changeName">Change Name</button> </div> </script>
فراخوانی کامپوننت نیز به اینصورت میباشد:
<div id="app"> <child :message="name"></child> </div>
همانطور که مشاهده میکنید، دیتای name را از طریق ویژگی message توانستهایم به کامپوننت child ارسال کنیم. درون تمپلیت آن نیز یک دکمه را برای تغییر مقدار این ویژگی تعریف کردهایم. تغییر این ویژگی نیز یک assignment ساده است. اما اگر بر روی دکمهی Change Name کلیک کنید، هشدار زیر را درون کنسول مشاهده خواهید کرد:
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "message" found in ---> <Child> <Root>
دلیل آن نیز مشخص است؛ زیرا با تغییر این ویژگی، کامپوننت والد از وجود تغییرات مطلع نشدهاست و فقط این تغییرات، درون کامپوننت child صورت گرفتهاست. برای اطلاعرسانی کامپوننت والد میتوانیم از یک ایونت ویژه استفاده کنیم و کامپوننت والد را از وجود تغییرات مطلع کنیم:
changeName() { this.message = "New Name!...", this.$emit("change-name", this.message); }
برای تگ child نیز این ایونت را اضافه خواهیم کرد:
<child :message="name" @change-name="name = $event"></child>
در اینحالت با تغییر ویژگی message، مقدار دیتای name نیز بلافاصله تغییر پیدا خواهد کرد.
Slots
Slot یک روش عالی برای جایگزینی محتوای درون یک کامپوننت است. فرض کنید میخواهیم کامپوننتمان به صورت زیر باشد:
<modal> Hello </modal>
در اینحالت باید درون تمپلیت مکان قرارگیری Hello را تعیین کنیم. اینکار را میتوانیم با قرار دادن تگ slot انجام دهیم:
Vue.component('modal', { template: ` ... <div class="modal-body"> <slot></slot> </div> ... ` });
اکنون هر محتوایی که درون تگ modal قرار گیرد، در قسمت slot نمایش داده خواهد شد. این نوع slot به صورت پیشفرض میباشد. در واقع میتوانیم slotها را نیز نامگذاری کنیم. به عنوان مثال یک slot برای عنوان modal، یک slot برای بدنه modal و یک slot دیگر برای فوتر modal تعریف کنیم:
Vue.component('modal', { template: ` <div class="modal fade" id="detailsModal" tabindex="-1" role="dialog" aria-labelledby="detailsModalLabel" aria-hidden="false"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="detailsModalLabel"> <slot name="title"></slot> </h5> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <slot name="body"></slot> </div> <div class="modal-footer"> <slot name="footer"></slot> </div> </div> </div> </div> ` });
اکنون میتوانیم محتوای مورد نظر را برای قرارگیری درون slotها تعیین کنیم:
<modal> <template slot="title">Title</template> <template slot="body">Lorem ipsum dolor sit amet.</template> <template slot="footer"> <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> <button type="button" class="btn btn-primary">Save changes</button> </template> </modal>
فرض کنید بخواهید به کاربر این امکان را بدهید تا بتواند چندین گزینه را برای یک فیلد انتخاب کند. به عنوان یک مثال ساده فرض کنید گزینه ای از مدل، پارچههای مورد علاقه یک نفر هست. کاربر میتواند چندین پارچه را انتخاب کند. و این فرض را هم بکنید که به لیست پارچهها گزینه دیگری اضافه نخواهد شد. پارچه (Fabric) را مثلا میتوانیم به صورت زیر تقسیم بندی کنیم :
- پنبه (Cotton)
- ابریشم (Silk)
- پشم (Wool)
- ابریشم مصنوعی (Rayon)
- پارچههای دیگر (Other)
با توجه به اینکه دیگر قرار نیست به این لیست گزینه دیگری اضافه شود میتوانیم آنرا به صورت یک نوع داده شمارشی (Enum) تعریف کنیم. مثلا بدین صورت:
public enum Fabric { [Description("پنبه")] Cotton, [Description("ابریشم")] Silk, [Description("پشم")] Wool, [Description("ابریشم مصنوعی")] Rayon, [Description("پارچههای دیگر")] Other }
حال فرض کنید View Model زیر فیلدی از نوع نوع داده شمارشی Fabric دارد:
public class MyViewModel { public Fabric Fabric { get; set; } }
توجه داشته باشید که فیلد Fabric از کلاس MyViewModel باید چند مقدار را در خود نگهداری کند. یعنی میتواند هر کدام از گزینههای Cotton، Silk، Wool، Rayon، Other به صورت جداگانه یا ترکیبی باشد. اما در حال حاضر با توجه به اینکه یک فیلد Enum معمولی فقط میتواند یک مقدار را در خودش ذخیره کند قابلیت ذخیره ترکیبی مقادیر در فیلد Fabric از View Model بالا وجود ندارد.
اما راه حل این مشکل استفاده از پرچم (Flags) در تعریف نوع داده شمارشی هست. با استفاده از پرچم نوع داده شمارشی بالا به صورت زیر باید تعریف شود:
[Flags] public enum Fabric { [Description("پنبه")] Cotton = 1, [Description("ابریشم")] Silk = 2, [Description("پشم")] Wool = 4, [Description("ابریشم مصنوعی")] Rayon = 8, [Description("پارچههای دیگر")] Other = 128 }
برای آشنایی بیشتر با این موضوع به کدهای زیر نگاه کنید:
Fabric cotWool = Fabric.Cotton | Fabric.Wool; int cotWoolValue = (int) cotWool;
حال فرض کنید فیلد Fabric از View Model ذکر شده (کلاس MyViewModel) را به صورت لیستی از چک باکسها نمایش دهیم. مثل زیر:
برای این کار از پروژه MVC Enum Flags کمک خواهیم گرفت. این پروژه شامل یک Html Helper برای تبدیل یه Enum به یک CheckBoxList و همچنین شامل Model Binder مربوطه هست. البته بعضی از کدهای Html Helper آن احتیاج به تغییر داشت که آنرا انجام دادم ولی بایندر آن بسیار خوب کار میکند.
خوب html helper مربوط به آن به صورت زیر میباشد:
public static IHtmlString CheckBoxesForEnumFlagsFor<TModel, TEnum>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TEnum>> expression) { ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData); Type enumModelType = metadata.ModelType; // Check to make sure this is an enum. if (!enumModelType.IsEnum) { throw new ArgumentException("This helper can only be used with enums. Type used was: " + enumModelType.FullName.ToString() + "."); } // Create string for Element. var sb = new StringBuilder(); foreach (Enum item in Enum.GetValues(enumModelType)) { if (Convert.ToInt32(item) != 0) { var ti = htmlHelper.ViewData.TemplateInfo; var id = ti.GetFullHtmlFieldId(item.ToString()); //Derive property name for checkbox name var body = expression.Body as MemberExpression; var propertyName = body.Member.Name; var name = ti.GetFullHtmlFieldName(propertyName); //Get currently select values from the ViewData model TEnum selectedValues = expression.Compile().Invoke(htmlHelper.ViewData.Model); var label = new TagBuilder("label"); label.Attributes["for"] = id; label.Attributes["style"] = "display: inline-block;"; var field = item.GetType().GetField(item.ToString()); // Add checkbox. var checkbox = new TagBuilder("input"); checkbox.Attributes["id"] = id; checkbox.Attributes["name"] = name; checkbox.Attributes["type"] = "checkbox"; checkbox.Attributes["value"] = item.ToString(); if ((selectedValues as Enum != null) && ((selectedValues as Enum).HasFlag(item))) { checkbox.Attributes["checked"] = "checked"; } sb.AppendLine(checkbox.ToString()); // Check to see if DisplayName attribute has been set for item. var displayName = field.GetCustomAttributes(typeof(DisplayNameAttribute), true) .FirstOrDefault() as DisplayNameAttribute; if (displayName != null) { // Display name specified. Use it. label.SetInnerText(displayName.DisplayName); } else { // Check to see if Display attribute has been set for item. var display = field.GetCustomAttributes(typeof(DisplayAttribute), true) .FirstOrDefault() as DisplayAttribute; if (display != null) { label.SetInnerText(display.Name); } else { label.SetInnerText(item.ToDescription()); } } sb.AppendLine(label.ToString()); // Add line break. sb.AppendLine("<br />"); } } return new HtmlString(sb.ToString()); }
public static string ToDescription(this Enum value) { var attributes = (DescriptionAttribute[])value.GetType().GetField(value.ToString()).GetCustomAttributes(typeof(DescriptionAttribute), false); return attributes.Length > 0 ? attributes[0].Description : value.ToString(); }
@Html.CheckBoxesForEnumFlagsFor(x => x.Fabric)
که باعث تولید خروجی که در تصویر (الف) نشان داده شد میشود. و همچنین مدل بایندر مربوط به آن به صورت زیر هست:
public class FlagEnumerationModelBinder : DefaultModelBinder { public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { if (bindingContext == null) throw new ArgumentNullException("bindingContext"); if (bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName)) { var values = GetValue<string[]>(bindingContext, bindingContext.ModelName); if (values.Length > 1 && (bindingContext.ModelType.IsEnum && bindingContext.ModelType.IsDefined(typeof(FlagsAttribute), false))) { long byteValue = 0; foreach (var value in values.Where(v => Enum.IsDefined(bindingContext.ModelType, v))) { byteValue |= (int)Enum.Parse(bindingContext.ModelType, value); } return Enum.Parse(bindingContext.ModelType, byteValue.ToString()); } else { return base.BindModel(controllerContext, bindingContext); } } return base.BindModel(controllerContext, bindingContext); } private static T GetValue<T>(ModelBindingContext bindingContext, string key) { if (bindingContext.ValueProvider.ContainsPrefix(key)) { ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(key); if (valueResult != null) { bindingContext.ModelState.SetModelValue(key, valueResult); return (T)valueResult.ConvertTo(typeof(T)); } } return default(T); } }
این مدل بایندر را باید به این صورت در متد Application_Start فایل Global.asax فراخوانی کنیم:
ModelBinders.Binders.Add(typeof(Fabric), new FlagEnumerationModelBinder());
مشاهده میکنید که در اینجا دقیقا مشخص کرده ایم که این مدل بایندر برای نوع داده شمارشی Fabric هست. اگر نیاز دارید تا این بایندر برای نوع دادههای شمارشی دیگری نیز به کار رود نیاز هست تا این خط کد را برای هر کدام از آنها تکرار کنید. اما راه حل بهتر این هست که کلاسی به صورت زیر تعریف کنیم و تمامی نوع دادههای شمارشی که باید از بایندر بالا استفاده کنند را در یک پراپرتی آن برگشت دهیم. مثلا بدین صورت:
public class ModelEnums { public static IEnumerable<Type> Types { get { var types = new List<Type> { typeof(Fabric) }; return types; } } }
سپس به متد Application_Start رفته و کد زیر را اضافه میکنیم:
foreach (var type in ModelEnums.Types) { ModelBinders.Binders.Add(type, new FlagEnumerationModelBinder()) }
اگر گزینههای پشم و ابریشم مصنوعی را از CheckBoxList تولید شده انتخاب کنیم، بدین صورت:
شکل (ب)
و سپس فرم را پست کنید، موردی شبیه زیر مشاهده میکنید:
همچنین مقدار عددی معادل در این جا برابر 12 میباشد که از جمع دو مقدار Wool و Rayon به دست آمده است. بدین ترتیب در یک فیلد از مدل، گزینههای انتخابی توسط کاربر قرار گرفته شده اند.
پروژه مربوط به این مثال را از لینک زیر دریافت کنید:
MvcEnumFlagsProjectSample.zip
پی نوشت : پوشههای bin و obj و packages جهت کاهش حجم پروژه از آن حذف شده اند. برای بازسازی پوشه packages لطفا به مطلب بازسازی کامل پوشه packages بستههای NuGet به صورت خودکار مراجعه کنید.