動態選單系統允許不重新編譯程式即可調整選單結構。本文示範在 WinForms 中讀取 XML 定義,透過反射動態建立 MenuStrip 並綁定事件處理器的完整架構。
開啟檔案
關閉檔案
剪下
複製
貼上
using System;
using System.IO;
using System.Reflection;
using System.Windows.Forms;
using System.Xml;
namespace DynamicMenuDemo
{
public class Menu
{
private string _path;
public string Path
{
get => _path;
set => _path = value;
}
public bool FileExists()
{
return File.Exists(_path);
}
public void LoadAllMenu(MenuStrip menuStrip)
{
if (!FileExists())
throw new FileNotFoundException("XML 選單檔案不存在", _path);
using (XmlTextReader reader = new XmlTextReader(_path))
{
while (reader.Read())
{
// 處理主選單
if (!reader.IsEmptyElement && reader.Name == "MainMenu")
{
string id = reader.GetAttribute("id");
string title = reader.GetAttribute("title");
var menuItem = new ToolStripMenuItem
{
Name = $"Item{id}",
Text = title
};
if (!string.IsNullOrEmpty(title))
menuStrip.Items.Add(menuItem);
}
// 處理子選單
else if (!reader.IsEmptyElement && reader.Name == "SubMenu")
{
string id = reader.GetAttribute("id");
string parentId = id.Substring(0, 2); // 前兩碼為父選單 ID
// 讀取子選單標題
string title = "";
using (XmlReader subReader = reader.ReadSubtree())
{
while (subReader.Read())
{
if (!subReader.IsEmptyElement && subReader.Name == "Title")
{
title = subReader.ReadElementContentAsString();
break;
}
}
}
if (string.IsNullOrEmpty(title))
continue;
var subItem = new ToolStripMenuItem
{
Name = $"Item{id}",
Text = title
};
subItem.Click += ToolSubItem_Click;
// 尋找父選單並加入
var parentItem = menuStrip.Items[$"Item{parentId}"] as ToolStripMenuItem;
parentItem?.DropDownItems.Add(subItem);
}
}
}
}
private void ToolSubItem_Click(object sender, EventArgs e)
{
if (sender is ToolStripMenuItem item)
{
// 透過反射呼叫對應方法
var methodClass = new MenuMethod();
MethodInfo method = methodClass.GetType().GetMethod(item.Name);
if (method != null)
{
method.Invoke(methodClass, null);
}
else
{
MessageBox.Show($"未實作方法:{item.Name}", "提示");
}
}
}
}
}
using System;
using System.Windows.Forms;
namespace DynamicMenuDemo
{
public class MenuMethod
{
public void Item101()
{
MessageBox.Show("執行:開啟檔案", "動作");
}
public void Item102()
{
MessageBox.Show("執行:關閉檔案", "動作");
}
public void Item111()
{
MessageBox.Show("執行:剪下", "動作");
}
public void Item112()
{
MessageBox.Show("執行:複製", "動作");
}
public void Item113()
{
MessageBox.Show("執行:貼上", "動作");
}
}
}
using System;
using System.Windows.Forms;
namespace DynamicMenuDemo
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
var menu = new Menu
{
Path = Path.Combine(Application.StartupPath, "Menu.xml")
};
try
{
if (menu.FileExists())
{
menu.LoadAllMenu(this.menuStrip1);
}
else
{
MessageBox.Show("找不到 Menu.xml 設定檔", "錯誤");
}
}
catch (Exception ex)
{
MessageBox.Show($"選單載入失敗:{ex.Message}", "錯誤");
}
}
}
}
// 遞迴處理任意深度選單
public void LoadMenuRecursive(XmlReader reader, ToolStripItemCollection parentCollection)
{
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element)
{
if (reader.Name == "MenuItem" || reader.Name == "SubMenu")
{
string id = reader.GetAttribute("id");
string title = reader.GetAttribute("title") ?? "";
// 讀取子標題 (若有)
if (reader.Name == "SubMenu" && reader.HasAttributes)
{
// 移至 Title 元素
while (reader.Read() && reader.Name != "Title") ;
if (reader.Name == "Title")
title = reader.ReadElementContentAsString();
}
var item = new ToolStripMenuItem
{
Name = $"Item{id}",
Text = title,
Tag = id // 儲存 ID 供後續使用
};
item.Click += ToolSubItem_Click;
parentCollection.Add(item);
// 遞迴處理子選單
if (reader.Name == "SubMenu" && !reader.IsEmptyElement)
{
LoadMenuRecursive(reader.ReadSubtree(), item.DropDownItems);
}
}
}
}
}
// ViewModel 基礎
public class MenuItemViewModel : INotifyPropertyChanged
{
public string Id { get; set; }
public string Text { get; set; }
public string IconPath { get; set; }
public ICommand Command { get; set; }
public List Children { get; set; } = new();
public event PropertyChangedEventHandler PropertyChanged;
}
// 命令實作
public class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Func _canExecute;
public RelayCommand(Action execute, Func canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object parameter) => _canExecute?.Invoke() ?? true;
public void Execute(object parameter) => _execute();
public event EventHandler CanExecuteChanged;
}
// XML 解析為 ViewModel
public List ParseMenuXml(string xmlPath)
{
var doc = XDocument.Load(xmlPath);
return doc.Root.Elements("MenuItem").Select(ParseMenuItem).ToList();
}
private MenuItemViewModel ParseMenuItem(XElement element)
{
var item = new MenuItemViewModel
{
Id = element.Attribute("id")?.Value,
Text = element.Attribute("text")?.Value,
IconPath = element.Attribute("image")?.Value,
Command = new RelayCommand(() => ExecuteAction(element.Attribute("action")?.Value))
};
item.Children = element.Elements("MenuItem").Select(ParseMenuItem).ToList();
return item;
}
| 面向 | 建議 |
|---|---|
| XML 路徑 | 使用 Application.StartupPath 或設定檔,避免硬編碼 |
| 錯誤處理 | XML 解析、反射呼叫皆需 try-catch,提供友善錯誤訊息 |
| 效能 | 應用程式啟動時一次性載入,避免重複讀取 |
| 擴充性 | 採用遞迴解析支援任意層級,預留 Tag/Action 機制 |
| 維護性 | 分離 XML 定義、載入邏輯、動作實作三層 |
| 現代化 | 新專案建議採用 MVVM + Command Pattern,配合 DI 容器 |
XML 動態選單透過「定義與程式碼分離」實現靈活配置。核心流程:XML 解析 → 建立 ToolStripMenuItem 階層 → 反射/命令綁定動作。傳統 WinForms 使用反射呼叫對應命名方法,現代架構建議改用 MVVM + Command Pattern,獲得更好的測試性、解耦合與維護性。無論採用何種架構,錯誤處理、路徑管理、擴充性設計都是關鍵成功因素。