Наши проекты:
Журнал · Discuz!ML · Wiki · DRKB · Помощь проекту |
||
ПРАВИЛА | FAQ | Помощь | Поиск | Участники | Календарь | Избранное | RSS |
[3.142.53.68] |
|
Сообщ.
#1
,
|
|
|
Команды в WPF, как быть и что с ними делать.
После празников немного тяжко думается, но всетаки вот решил сесть за написание статьи. Mаршрутизированные события и маршрутизированные команды это одни из самых интересных нововведения в WPF, которые требуют особого внимания и освоения. Как извесно в WPF и C# 3.0 реализованны многие паттерны проектирования, но к сожалению (сугубо по моему личному мнению) есть много неудобных моментов и сложностей в применении и освоении. Так в проекте в котором я участвую было принято решение широко использовать маршрутизированные события и маршрутизированные команды, но сразу же были замечены сложности и неудобные моменты усложняющие поддержку, а в часности сильно увеличивающие код. Более подробно не буду на этом останавливаться, а лучше перейду к описанию решения к которому я пришол для упрощения работы с маршрутизированными командами. Идея заключается примерно в следующем, исключить появления множества команд в разных фреймах приложения и ценрализовать все команды в одном контейнере и автоматизировать создание команд для фреймов ( сам не понял что написал, но как-то так ). Теперь непосредственно перейдем к реализации. В проекте используется паттерн DM-V-VM о котором уже много написанно на просторах рунета, так что в примере который я приведу он так же будет использован, так же использован MS EnterpriseLibs выпуст октябрь 2008, упрощающий применение паттерна DM-V-VM. Первым делом опишу шаблон будующего примера, т.е. скажем так, мы имеем некоторое главное окно Window1, которое представляет собой контейнер для наших загружаемых фреймов, и непосредственно подопытный SampleFrame. Для SampleFrame создаем презентера SampleFrameVM, ниже привожу полный код для обоих объектов нашего приложения. XAML код SampleFrame: <UserControl x:Class="CommandAttributeExp.SampleFrame" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Commands="clr-namespace:CommandAttributeExp.Commands" MinHeight="200" MinWidth="300" Focusable="True"> <UserControl.InputBindings> <KeyBinding Key="F5" Command="Commands:CustomCommands.Refresh"/> <KeyBinding Key="F6" Command="Commands:CustomCommands.Refresh" CommandParameter="Привет"/> <KeyBinding Key="F7" Command="Commands:CustomCommands.Refresh" CommandParameter="12354"/> </UserControl.InputBindings> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition/> </Grid.RowDefinitions> <DockPanel> <ToolBar> <Button Width="60" Height="20" ToolTip="Создать что то. Insert" Command="Commands:CustomCommands.NewDoc">NewDoc</Button> </ToolBar> </DockPanel> <StackPanel Grid.Row="1"> <TextBlock FontSize="14">Button Events</TextBlock> <DockPanel HorizontalAlignment="Left"> <Button Command="ApplicationCommands.Save" Width="100">SaveCommand</Button> </DockPanel> <TextBlock Margin="5">По F5 Refresh</TextBlock> <TextBlock Margin="5">По F6 Refresh c передачей строкового параметра</TextBlock> <TextBlock Margin="5,5,5,0">По F7 Refresh должен был по идее передаваться с числовым параметром</TextBlock> <TextBlock Margin="5,0,5,5"> но проблема в том что параметр всегда в строковом виде передается</TextBlock> </StackPanel> <ListView Grid.Row="2" ItemsSource="{Binding Path=Messages}" SnapsToDevicePixels="True"> </ListView> </Grid> </UserControl> Код презентера сразу приведу в готовом виде, как выглядит реализация команд в интерпритации Microsoft я думаю все итак не раз видели. using System; using System.Collections.ObjectModel; using System.ComponentModel; using CommandAttributeExp.Commands; namespace CommandAttributeExp { public class SampleFrameVM : INotifyPropertyChanged { private ObservableCollection<string> messages; public ObservableCollection<string> Messages { get { return messages; } set { messages = value; OnPropertyChanged("Message"); } } public SampleFrameVM() { Messages = new ObservableCollection<string>(); } [Command("Save")] public void DoSaveAction() { Messages.Add(string.Format("Обработана команда Save {0}", DateTime.Now.TimeOfDay)); } [Command("Refresh")] public void Refresh() { Messages.Add(string.Format("Обработана команда Refresh {0}", DateTime.Now.TimeOfDay)); } [Command("Refresh", typeof(string))] public void Refresh(string paramString) { Messages.Add(string.Format("Обработана команда Refresh со строковым параметром {0}", DateTime.Now.TimeOfDay)); } [Command("Refresh", typeof(int))] public void Refresh(int paramInt) { Messages.Add(string.Format("Обработана команда Refresh с числовым параметром {0}", DateTime.Now.TimeOfDay)); } [Command("NewDoc")] public void CreateDoc() { Messages.Add(string.Format("Обработана команда CreateDoc {0}", DateTime.Now.TimeOfDay)); } [MultiCommand(new[] { "NewDoc", "Save" })] public void CreateDocAndSave() { Messages.Add(string.Format("Этот метод реагирует на две команды Save и NewDoc {0}", DateTime.Now.TimeOfDay)); } #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged(string propName) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propName)); } #endregion } } Так вот, что нам надо чтобы наши аттрибуты заработали, конечно-же клас описывающий эти самые аттрибуты, вот его код: public class CommandAction { public static string Save = "Save"; public static string Open = "Open"; public static string EditDoc = "EditDoc"; public static string Refresh = "Refresh"; public static string NewDoc = "NewDoc"; } [AttributeUsage(AttributeTargets.Method, Inherited = false)] public class CommandAttribute : Attribute { public CommandAttribute(string action) { HasParam = false; Action = action; } public CommandAttribute(string action, Type paramType) { Action = action; HasParam = true; ParamType = paramType; } public Type ParamType { get; set; } public bool HasParam { get; private set; } public string Action { get; set; } } Немного подробнее о нем, класс CommandAction это перечисление команд и их строковые метки которые будут использованны в нашем тестовом проекте. Что позволяет нам сделать наш аттрибут, во первых пометить наш метод так что какойто класс реализующий логику команд будет знать что наш метод ждет вызова определенной команды и так же может ли он принять параметр передаваемый командой или нет. Тут есть одно но, все параметры передаваемые комадами строковые (в свете теплых чуств MS к object мне это по меньшей мере показалось странным) что немного нас сковывает в возможностях. Что далее, аттрибут мы имеем, так же имеем методы ожидающие что их позавут и попросят что либо выполнить, но кто же должен их звать? Для этих целей служит клас названный мной CommandsCollector, вот его код: using System; using System.Reflection; using System.Windows; using System.Windows.Input; namespace CommandAttributeExp.Commands { public class CommandsCollector { public void CommandExecuted(object target, ExecutedRoutedEventArgs e) { var sender = e.Source; var x = ((FrameworkElement) sender).DataContext.GetType(); var comName = ((RoutedUICommand) e.Command).Text; foreach (MethodInfo mInfo in x.GetMethods()) foreach (Attribute attr in Attribute.GetCustomAttributes(mInfo)) { if (attr.GetType() == typeof(CommandAttribute) ) { var at = (CommandAttribute)attr; if (at.Action == comName && !at.HasParam && e.Parameter == null) mInfo.Invoke(((FrameworkElement)sender).DataContext, new object[] { }); if (at.Action == comName && at.HasParam && e.Parameter != null) { if (e.Parameter.GetType() == at.ParamType) mInfo.Invoke(((FrameworkElement)sender).DataContext, new[] { e.Parameter }); } } if(attr.GetType() == typeof(MultiCommandAttribute)) { var mat = (MultiCommandAttribute)attr; foreach (var act in mat.Actions) { if (act == comName && !mat.HasParam) { mInfo.Invoke(((FrameworkElement)sender).DataContext, new object[] { }); } } } } } public void CommandCanExecute(object sender, CanExecuteRoutedEventArgs e) { var s = e.Source; var x = ((FrameworkElement)s).DataContext.GetType(); var comName = ((RoutedUICommand)e.Command).Text; foreach (MethodInfo mInfo in x.GetMethods()) foreach (Attribute attr in Attribute.GetCustomAttributes(mInfo)) if (attr.GetType() == typeof(CommandAttribute) && ((CommandAttribute)attr).Action == comName) e.CanExecute = true; } } } Этот клас просто через Reflection смотрит класс и выдергивает из него методы помеченные нашим аттрибутом и если совпадает Action выполняет этот метод. Остался последний вопрос, давайте вернемся к коду SampleFrame, что мы видим там интересного, есть InputBindings, но нет привязки к выполняемым по команде методам, а теперь они не нужны, все привязки команд обявляются и хрянятся в контейнере созданном при запуске приложения и привязываются каждому вновь создаваемому фрейму при его загрузке по наличию атрибутов в презентере, вот так все хитро. private void Application_Startup(object sender, StartupEventArgs e) { var mw = new Window1(); var coll = new CommandsCollector(); IUnityContainer Cfg = new UnityContainer(); Cfg.RegisterInstance(Cfg). RegisterInstance<IWorkplace>(mw). RegisterInstance(coll). RegisterInstance<IDictionary<string, CommandBinding>>(new Dictionary<string, CommandBinding> { { CommandAction.Save, new CommandBinding(ApplicationCommands.Save, coll.CommandExecuted, coll.CommandCanExecute) }, { CommandAction.Refresh, new CommandBinding(CustomCommands.Refresh, coll.CommandExecuted) }, { CommandAction.NewDoc, new CommandBinding(CustomCommands.NewDoc, coll.CommandExecuted) } }). RegisterType<IView<SampleFrameVM>, SampleFrame>(); Cfg.BuildUp(mw); mw.Show(Cfg.Resolve<SampleFrameVM>()); mw.Show(); } P.S.: писатель из меня никудышный согласен, но описал как мог, думаю всем будет приятней почитать код чем тот бред который я здесь изложил , в этом случае код в аттаче........ Прикреплённый файлCommandAttribute.zip (210.15 Кбайт, скачиваний: 971) |