كيفية إنشاء مقتطفات أكواد C# تلقائياً في Visual Studio
مقدمة: لماذا تُعد مقتطفات الأكواد أداة فعّالة للمطورين؟
عند العمل داخل بيئة Visual Studio، يمكن لمقتطفات الأكواد Code Snippets أن تختصر كثيراً من الوقت والجهد، لأنها تتيح لك تحويل اختصارات قصيرة إلى شيفرات جاهزة ومتكاملة. وهذا لا يسرّع الكتابة فحسب، بل يساعد أيضاً على تقليل الأخطاء المتكررة وتحسين الإنتاجية أثناء التطوير.
في هذا الدليل، سنشرح مفهوم مقتطفات الأكواد في C#، ثم ننتقل إلى كيفية إنشاء مقتطفات مخصصة، وبعد ذلك نستعرض فكرة متقدمة لإنشاء هذه المقتطفات تلقائياً اعتماداً على الأنواع Types والمساحات الاسمية Namespaces المستخدمة داخل المشروع.

ما هي مقتطفات الأكواد في Visual Studio؟
مقتطف الكود هو اختصار يمثل جزءاً أكبر من الشيفرة. بدلاً من كتابة التعليمات كاملةً يدوياً، يمكنك إدخال كلمة قصيرة ثم الضغط على مفتاح Tab مرتين ليقوم Visual Studio بإدراج الشيفرة المناسبة تلقائياً.
على سبيل المثال، إذا كتبت for ثم ضغطت Tab مرتين داخل مشروع C#، فسيتم إنشاء حلقة for جاهزة. وكذلك عند كتابة cw ثم الضغط على Tab مرتين، ستحصل على التعليمة التالية:
Console.WriteLine();
توجد مجموعة كبيرة من المقتطفات الجاهزة مسبقاً داخل Visual Studio، ويمكنك كذلك إنشاء مقتطفاتك الخاصة بما يناسب طريقة عملك.
كيفية إنشاء مقتطف كود مخصص في C#
لبناء أول مقتطف مخصص، أنشئ ملفاً بامتداد .snippet، وليكن مثلاً MySnippet.snippet. بعد ذلك افتح الملف داخل Visual Studio وأضف البنية الأساسية التالية:
<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="">
<CodeSnippet Format="1.0.0">
<Header>
<Title></Title>
<Author></Author>
<Description></Description>
<Shortcut></Shortcut>
</Header>
<Snippet>
<Code Language="">
<![CDATA[]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>
هذه هي البنية الأساسية لأي ملف مقتطف. وسنحوّلها الآن إلى مقتطف يقوم بتحويل الاختصار cr إلى Console.ReadLine();.
شرح الوسوم الأساسية داخل ملف المقتطف
<Title>: اسم المقتطف الظاهر، وليس الاختصار نفسه.<Author>: اسم الكاتب أو الجهة المطورة.<Description>: وصف مختصر لما يقوم به المقتطف.<Shortcut>: الاختصار الذي تستدعي به الشيفرة.<Code Language="CSharp">: تحديد لغة المقتطف، وهنا نستخدمCSharp.
مثال شائع:
cw -> Console.WriteLine();
في هذه الحالة تكون قيمة <Shortcut> هي cw.
أما الشيفرة الفعلية التي سيولدها المقتطف فتُكتب داخل القسم <![CDATA[]]> بهذا الشكل:
<Code Language="CSharp">
<![CDATA[Console.ReadLine();]]>
</Code>
إضافة متغيرات داخل مقتطفات الأكواد
من الميزات المهمة في ملفات .snippet أنك تستطيع تعريف متغيرات قابلة للتعديل. هذا يتيح للمستخدم تغيير قيمة افتراضية واحدة، فتتحدث كل المواضع المرتبطة بها تلقائياً.
لنفترض أنك تريد مقتطفاً يولد مصفوفة مع حلقة تهيئة، مثل المثال التالي:
Object[] arr = new Object[100];
for (int i = 0; i < arr.Length; i++)
{
arr[i] = default(Object);
}
إذا عرّفت Object كمتغير، فسيكون بإمكانك تغييره إلى int أو أي نوع آخر، وسيتم تعديل جميع المواضع ذات الصلة تلقائياً. وإذا عرّفت arr أيضاً كمتغير، فستتمكن من التنقل بين المتغيرات باستخدام مفتاح Tab.
مثال كامل لتعريف متغيرات داخل مقتطف
<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="">
<CodeSnippet Format="1.0.0">
<Header>
<Title>init array.</Title>
<Author>Gilad Bar-Ilan</Author>
<Description>creates and initializes an array.</Description>
<Shortcut>myArr</Shortcut>
</Header>
<Snippet>
<Code Language="CSharp">
<![CDATA[$typeName$[] $varName$ = new $typeName$[100];
for(int $i$ = 0; $i$ < $varName$.Length; $i$++)
{
$varName$[$i$] = default($typeName$);
}]]>
</Code>
<Declarations>
<Literal>
<ID>varName</ID>
<ToolTip>variable name.</ToolTip>
<Default>arr</Default>
</Literal>
<Literal>
<ID>typeName</ID>
<ToolTip>type name.</ToolTip>
<Default>Object</Default>
</Literal>
<Literal>
<ID>i</ID>
<ToolTip>iterator name.</ToolTip>
<Default>i</Default>
</Literal>
</Declarations>
</Snippet>
</CodeSnippet>
</CodeSnippets>
كيف تعمل عناصر التصريح عن المتغيرات؟
<Declarations>: الحاوية التي تضم المتغيرات.<Literal>: تعريف متغير واحد.<ID>: الاسم المستخدم داخل<![CDATA[]]>.<ToolTip>: وصف قصير للمتغير.<Default>: القيمة الافتراضية.
وعند استدعاء المتغير داخل الشيفرة، يجب كتابته بهذه الصيغة:
$variableName$
كيفية إضافة ملف المقتطف إلى Visual Studio
بعد إنشاء ملف .snippet، تحتاج إلى وضعه داخل مجلد المقتطفات المخصصة في Visual Studio حتى يصبح متاحاً في مشاريعك.
- افتح مشروع
C#. - انتقل إلى
ToolsثمCode Snippets Manager. - انسخ المسار الظاهر في خانة
Location. - ضع ملف المقتطف داخل ذلك المسار.

تنبيه مهم: قد يختلف موقع مجلد My Code Snippets من جهاز إلى آخر، لذلك لا تعتمد على مسار ثابت.
ما المقصود بإنشاء مقتطفات الأكواد تلقائياً؟
بما أن إضافة ملفات .snippet إلى مجلد My Code Snippets تجعلها متاحة تلقائياً داخل Visual Studio، يمكن استغلال هذا السلوك لإنشاء برنامج يقوم ببناء هذه الملفات وإدراجها آلياً دون تدخل يدوي في كل مرة.
الفكرة هنا هي إنشاء مقتطف لكل نوع Type يملك مُنشئاً افتراضياً Default Constructor، بحيث يكون اسم الاختصار هو اسم النوع نفسه. مثال:
Random + Tab twice -> Random random = new Random();
بهذا الأسلوب، يستطيع البرنامج تحليل المشروع، والتعرف على الأنواع المناسبة، ثم توليد مقتطفات جاهزة لها.
متطلبات برنامج التوليد التلقائي للمقتطفات
- إضافة مقتطفات للأنواع الموجودة في المساحات الاسمية المستخدمة.
- إضافة مقتطفات للأنواع المعرفة من قبل المطور داخل المشروع.
- حذف المقتطفات التي لم تعد مطلوبة.
- تحديث المقتطفات في كل مرة يتم فيها تشغيل البرنامج.
- إضافة مقتطفات جديدة عند إدخال
namespaceجديد أو إنشاء أنواع جديدة.
كيفية الحصول على الأنواع المعرفة داخل المشروع
هناك طريقتان رئيسيتان للحصول على الأنواع المخصصة التي أنشأها المطور:
1) استخدام التعبيرات النمطية Regex
يمكن المرور على ملفات المشروع والبحث عن تعريفات الأصناف classes والبنى structs واكتشاف ما إذا كانت تحتوي على مُنشئ افتراضي. لكن هذه الطريقة ليست مثالية، لأنها تعتمد على تحليل النصوص مباشرة، وقد تعيد قراءة ملفات كثيرة لمجرد حدوث تعديلات طفيفة لا تؤثر على الأنواع.
2) استخدام الانعكاس Reflection
هذه الطريقة أكثر دقة ومرونة، لأنها تقرأ البيانات الوصفية Metadata الخاصة بالتجميع Assembly بدلاً من تحليل الملفات النصية سطراً بسطر.
فيما يلي دالة تسترجع الأنواع المعرفة داخل المشروع:
/// <summary>
/// Returns a list of all the types we created.
/// </summary>
/// <returns></returns>
public static List<Type> GetCustomTypes()
{
StackTrace myStackTrace = new StackTrace();
string namespace_ = myStackTrace.GetFrame(myStackTrace.FrameCount - 1).GetMethod().DeclaringType.Namespace;
Assembly assembly = Assembly.GetExecutingAssembly();
var types = assembly.GetTypes()
.Where(x => x.Namespace == namespace_)
.Where(x => !x.Name.Contains("<>c"))
.Where(x => x.GetConstructors().Any(x => x.GetParameters().Length == 0));
return types.ToList();
}
التحديات عند جلب أنواع المكتبات المستخدمة
الجزء الأكثر تعقيداً في المشروع هو جلب الأنواع التابعة للمكتبات والمساحات الاسمية المستخدمة داخل ملفات المشروع.
المشكلة الأولى
إذا استخدمت Reflection لقراءة التجميعات المحملة فقط، فلن تحصل على أنواع المساحات الاسمية التي تمت إضافتها عبر using لكنها لم تُستخدم فعلياً وقت التشغيل.
using System.Linq;
using System.IO;
public static void Main()
{
int[] arr = Enumerable.Range(0, 100).ToArray();
}
في المثال السابق، قد يظهر System.Linq فقط لأنه مستخدم فعلياً، بينما لا يظهر System.IO رغم وجوده في using.
المشكلة الثانية
قد يتوزع namespace واحد على عدة ملفات Assembly، والـمُصرّف Compiler لا يحملها كلها ما لم يحتج إليها. لذلك، حتى إن كنت تستخدم مكتبة ما، فقد لا تحصل على جميع أنواعها من خلال فحص ما تم تحميله فقط أثناء التنفيذ.
الحل المقترح: خوارزمية تجمع بين تحليل الملفات والانعكاس
لحل هذه المشكلة، يمكن اعتماد نهج عملي كالتالي:
- قراءة جميع ملفات
.csفي المشروع. - استخراج جميع المساحات الاسمية المعرفة في أوامر
using. - فحص ملفات
System DLLومحاولة مطابقة الأنواع التابعة لهذه المساحات الاسمية. - إضافة الأنواع المعرفة داخل المشروع.
- إضافة الأنواع القادمة من المراجع
Referencesالتي أضيفت للمشروع حتى إن لم تُستخدم مباشرة وقت التشغيل. - الاحتفاظ فقط بالأنواع التي تملك مُنشئاً افتراضياً أو الأنواع القيمية
Value Types.
الكود التالي يطبق هذه الفكرة:
using System;
using System.Reflection;
using System.Linq;
using System.IO;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.RegularExpressions;
namespace Reader
{
public class TypeReader
{
public static List<Type> GetAllTypes()
{
#region GetSystemLibrary Files
string systemDllPath = typeof(string).Assembly.Location;
systemDllPath = Path.GetDirectoryName(systemDllPath);
string[] systemLibraries = Directory.GetFiles(systemDllPath);
#endregion
#region Namespaces in project files.
string[] namespaces_ = Directory.GetFiles(GetSourceCodePath())
.Where(x => x.EndsWith(".cs")).ToArray();
namespaces_ = GetNamespaces(namespaces_).ToArray();
#endregion
#region Check the system libraries.
List<Type> types = systemLibraries.Where(x => x.EndsWith(".dll"))
.Select(x =>
{
Assembly asm = null;
try
{
asm = Assembly.LoadFile(x);
}
catch
{
asm = null;
}
return asm;
}).SelectMany(x => x != null ? x.GetTypes() : new Type[] { })
.Where(x => x != null && !x.Name.Contains("<>c"))
.Where(x => namespaces_.Contains(x.Namespace))
.Where(x => x.IsPublic && (x.IsClass || x.IsValueType)).ToList();
#endregion
#region add assemblies we use in our program.
types.AddRange(AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(x => x.GetTypes().Where(x => !x.Name.Contains("<>c")).Where(x => namespaces_.Contains(x.Namespace)))
.Where(x => x.IsPublic && (x.IsClass || x.IsValueType)));
#endregion
#region add custom types we created.
types.AddRange(GetCustomTypes());
#endregion
#region add types belongs to refrences we didn't use in our project.
types.AddRange(GetUnUsedRefrenecedTypes(namespaces_));
#endregion
#region filter types.
types = types.Where(x => x.IsValueType || x.GetConstructors().Any(x => x.GetParameters().Length == 0)).ToList();
#endregion
#region remove duplicated type names.
types = types.GroupBy(x => x.FullName).Select(x => x.First()).ToList();
#endregion
return types;
}
public static string GetMainAssemblyLocation()
{
StackTrace main = new StackTrace();
string path = main.GetFrame(main.FrameCount - 1).GetMethod().DeclaringType.Assembly.Location;
return Path.GetDirectoryName(path);
}
public static string GetSourceCodePath()
{
string projectPath = GetMainAssemblyLocation();
try
{
while (!Directory.GetFiles(projectPath).Any(x => x.EndsWith(".cs")))
{
projectPath = projectPath.Remove(projectPath.LastIndexOf("\\"));
}
}
catch
{
throw new SourceCodeNotFoundException();
}
return projectPath;
}
public static List<string> GetNamespaces(string[] files)
{
List<string> namespaces = new List<string>();
foreach (string file_ in files)
{
StreamReader fileStream = new StreamReader(file_);
Regex matchUsing = new Regex(@"using([ ]|\t|\n)+\S+;");
string[] fileNamespaces = matchUsing.Matches(fileStream.ReadToEnd()).Select(x => x.Value)
.Select(x => x.Remove(x.IndexOf("using"), "using".Length).Trim()).ToArray();
namespaces.AddRange(fileNamespaces);
fileStream.Close();
}
return namespaces.Select(x => x.Remove(x.Length - 1, 1)).Distinct().ToList();
}
public static List<Type> GetUnUsedRefrenecedTypes(string[] namespaces_)
{
string sourceCode = GetMainAssemblyLocation();
List<Type> otherTypes = new List<Type>();
string[] fileNames = Directory.GetFiles(Path.GetDirectoryName(sourceCode));
if (fileNames.Any(x => x.EndsWith(".dll")))
{
fileNames = fileNames.Where(x => x.EndsWith(".dll")).ToArray();
foreach (var file in fileNames)
{
Assembly asm;
try
{
asm = Assembly.LoadFile(file);
}
catch
{
continue;
}
otherTypes.AddRange(asm.GetTypes().Where(x => !x.Name.Contains("<>c"))
.Where(x => x.IsPublic && (x.IsClass || x.IsValueType))
.Where(x => namespaces_.Contains(x.Namespace)).GroupBy(x => x.Name)
.Select(x => x.First()));
}
}
return otherTypes;
}
public static List<Type> GetCustomTypes()
{
StackTrace myStackTrace = new StackTrace();
Type mainType = myStackTrace.GetFrame(myStackTrace.FrameCount - 1).GetMethod().DeclaringType;
string namespace_ = mainType.Namespace;
Assembly assembly = mainType.Assembly;
var types = assembly.GetTypes().Where(x => x.Namespace == namespace_).Where(x => !x.Name.Contains("<>c"));
return types.ToList();
}
}
internal class SourceCodeNotFoundException : Exception
{
public override string Message => "Could not load the '.cs' files, please make sure\n" +
"your files located in the default place / you have a valid project type.";
}
}
ملاحظة مهمة حول الدالة الرئيسية
عندما يشار في الشرح إلى main function فالمقصود هو دالة مثل:
public static void Main()
{
TypeReader.GetAllTypes();
}
أي أن الدالة الرئيسية ليست داخل الصنف TypeReader نفسه، بل هي التي تستدعيه.
شرح مبسط لأجزاء الكود السابقة
قسم GetSystemLibrary Files
يحدد هذا القسم مسار مكتبات النظام عبر الاستناد إلى موقع تجميع النوع string، ثم يقرأ ملفات .dll الموجودة في هذا المجلد.
قسم المساحات الاسمية داخل ملفات المشروع
في هذا القسم يتم جمع جميع ملفات .cs، ثم استخراج المساحات الاسمية المستخدمة في تعليمات using عبر Regex.
الدوال المساعدة
GetSourceCodePath(): تحاول الوصول إلى مجلد الشيفرة المصدرية انطلاقاً من موقع ملف التجميع الرئيسي.GetMainAssemblyLocation(): تستخدمStackTraceلتحديد موقع التجميع المرتبط بالدالة الرئيسية.GetNamespaces(): تقرأ أوامرusingمن الملفات وتعيد قائمة بالمساحات الاسمية المستخدمة.
تصفية الأنواع
بعد جمع الأنواع من مصادر متعددة، يتم الاحتفاظ فقط بالأنواع العامة public التي تمثل أصنافاً classes أو أنواعاً قيمية value types، ثم تصفيتها بناءً على وجود مُنشئ افتراضي، وأخيراً حذف التكرارات.
بناء ملفات المقتطفات تلقائياً
بعد الحصول على قائمة الأنواع المناسبة، تأتي خطوة إنشاء ملفات .snippet نفسها.
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using Reader;
namespace SnippetGenerator2
{
public class CodeSnippetGenerator
{
public string SnippetLocation;
public CodeSnippetGenerator(string snippetLocation)
{
this.SnippetLocation = snippetLocation.EndsWith("\\") ? snippetLocation : snippetLocation + "\\";
}
public void UpdateSnippets()
{
List<Type> allTypes = TypeReader.GetAllTypes();
Func<Type, string> makeFileName = x => SnippetLocation + x.Name + "-" + x.Namespace + ".snippet";
DirectoryInfo myCodeSnippets = new DirectoryInfo(SnippetLocation);
string[] snippetFileNames = allTypes.Select(x => makeFileName(x)).ToArray();
string[] previous = myCodeSnippets.GetFiles().Where(x => x.Name.EndsWith(".snippet")).Select(x => x.Name).ToArray();
previous = previous.Where(x => !snippetFileNames.Contains(x)).ToArray();
previous.ToList().ForEach(x =>
{
try
{
File.Delete(SnippetLocation + x);
}
catch { }
});
allTypes.ForEach(x =>
{
if (!Directory.GetFiles(SnippetLocation).Contains(makeFileName(x)))
CreateSnippet(x);
});
}
public List<string> RemoveAllCodeSnippets()
{
string[] files = Directory.GetFiles(SnippetLocation).ToArray();
List<string> notDeleted = new List<string>();
files.ToList().ForEach(x =>
{
StreamReader snippetFile = new StreamReader(x);
if (snippetFile.ReadToEnd().Contains("<Author>AUTO-GENERATOR</Author>"))
{
snippetFile.Close();
try
{
File.Delete(x);
}
catch
{
notDeleted.Add(x);
}
}
});
int i = 0;
while (i < notDeleted.Count)
{
i++;
try
{
File.Delete(notDeleted[i]);
}
catch
{
continue;
}
notDeleted.Remove(notDeleted[i]);
}
return notDeleted;
}
internal void CreateSnippet(Type type, string description = "DefaultDescription")
{
string snippet_structure = $@"<?xml version=""1.0"" encoding=""utf-8""?>
<CodeSnippets xmlns = """">
<CodeSnippet Format = ""1.0.0"">
<Header>
<Title>{type.Name} - {type.Namespace}</Title>
<Author>AUTO-GENERATOR</Author>
<Description>{description}</Description>
<Shortcut>{type.Name}</Shortcut>
</Header>
<Snippet>
<Code Language = ""CSharp"">
<![CDATA[
{type.Name} $obj$ = new {type.Name}();]]>
</Code>
<Declarations>
<Literal>
<ID>obj</ID>
<ToolTip>variable name.</ToolTip>
<Default>{type.Name.ToLower()}</Default>
</Literal>
</Declarations>
</Snippet>
</CodeSnippet>
</CodeSnippets>";
File.WriteAllText(SnippetLocation + type.Name + "-" + type.Namespace + ".snippet", snippet_structure);
}
}
}
كيف تعمل فئة توليد المقتطفات؟
اسم ملف المقتطف
يتم حفظ الملف بالصيغة التالية:
<TypeName>-<NamespaceName>.snippet
الدالة البانية Constructor
تستقبل مسار مجلد My Code Snippets وتضمن أن يكون تنسيقه صالحاً للاستخدام عند إنشاء الملفات.
الدالة CreateSnippet()
تنشئ بنية مقتطف موحدة لكل نوع، بحيث يكون الاختصار هو اسم النوع، وتكون الشيفرة الناتجة بالشكل التالي:
TypeName obj = new TypeName();
كما يتم تعريف متغير obj بقيمة افتراضية مشتقة من اسم النوع بحروف صغيرة.
الدالة UpdateSnippets()
تتولى تحديث المقتطفات في كل تشغيل عبر الخطوات التالية:
- قراءة جميع الأنواع المتاحة من خلال
TypeReader.GetAllTypes(). - تكوين أسماء الملفات المتوقعة للمقتطفات.
- قراءة ملفات المقتطفات القديمة من المجلد.
- حذف الملفات التي لم تعد مرتبطة بأي نوع حالي.
- إنشاء ملفات جديدة للأنواع التي لا تملك مقتطفاً بعد.
الدالة RemoveAllCodeSnippets()
تحذف جميع المقتطفات التي أنشأها المولد التلقائي فقط، وذلك عبر البحث عن القيمة AUTO-GENERATOR داخل وسم <Author>. وبهذه الطريقة لا يتم المساس بالمقتطفات التي أنشأها المستخدم يدوياً.
كيفية استخدام برنامج التوليد التلقائي للمقتطفات
لاستخدام هذا النظام داخل مشروعك، أضف في بداية الدالة الرئيسية الكود التالي:
var csg = new CodeSnippetGenerator("path of My Code Snippets");
csg.UpdateSnippets();
// لإنشاء المقتطفات وتحديثها
csg.RemoveAllCodeSnippets();
// لحذف المقتطفات التي أنشأها المولد
بعد ذلك، في كل مرة تشغّل فيها البرنامج، سيتم إنشاء المقتطفات الجديدة وحذف غير الضروري منها وفق حالة المشروع الحالية.
أفضل الممارسات عند تطبيق هذه الفكرة
- احرص على اختبار المقتطفات داخل مشروع تجريبي قبل اعتمادها في بيئة العمل الأساسية.
- استخدم أسماء اختصارات واضحة وغير متعارضة مع المقتطفات المدمجة في
Visual Studio. - إذا كان مشروعك كبيراً، ففكر في تقليل عدد عمليات الفحص لتقليل زمن التشغيل.
- ميّز المقتطفات المُولدة تلقائياً بقيمة ثابتة في
<Author>لتسهيل حذفها وإدارتها. - راجع الأنواع المستخرجة من المكتبات الخارجية لتجنب توليد مقتطفات كثيرة غير مفيدة.
الخلاصة التقنية
إنشاء مقتطفات أكواد مخصصة في Visual Studio يمنح المطور سرعة ملحوظة في كتابة الشيفرة، لكن بناء نظام يولّد هذه المقتطفات تلقائياً يرفع مستوى الأتمتة إلى درجة أكثر احترافية. تقنياً، يُعد الجمع بين Reflection وتحليل أوامر using حلاً عملياً لتوسيع نطاق المقتطفات بحيث يشمل الأنواع المحلية والمكتبات الخارجية. ورغم أن هذا النهج ليس الأبسط من حيث التنفيذ، فإنه مفيد جداً للمشاريع التي تعتمد على تكرار إنشاء كائنات وأنماط شيفرة متشابهة بشكل مستمر.