I created a custom class called BrowseButton which extends Button. This button is fairly simple; when clicked it pops up a file chooser dialog. I created it as its own special class because I wanted to be able to re-use it quickly and easily in my applications. After the user successfully selects a file, I also want it to populate a TextBox control on the same page with the full file path.
Here's what my (C#) code looks like for the button:
using System;
using System.Windows;
using System.Windows.Controls;
using Microsoft.Win32;
namespace MyProject.Extensions
{
public partial class BrowseButton : Button
{
public static readonly DependencyProperty DefaultExtDependency = DependencyProperty.Register("DefaultExt", typeof(string), typeof(BrowseButton));
public static readonly DependencyProperty FilterDependency = DependencyProperty.Register("Filter", typeof(string), typeof(BrowseButton));
public static readonly DependencyProperty TextBoxDependency = DependencyProperty.Register("TextBox", typeof(TextBox), typeof(BrowseButton));
public string DefaultExt
{
get
{
return (string)GetValue(DefaultExtDependency);
}
set
{
SetValue(DefaultExtDependency, value);
}
}
public string Filter
{
get
{
return (string)GetValue(FilterDependency);
}
set
{
SetValue(FilterDependency, value);
}
}
public TextBox TextBox
{
get
{
return (TextBox)GetValue(TextBoxDependency);
}
set
{
SetValue(TextBoxDependency, value);
}
}
public BrowseButton()
{
InitializeComponent();
}
public event EventHandler<string> FileSelected;
public void Connect(int connectionId, object target)
{
}
private void BrowseButton_OnClick(object sender, RoutedEventArgs e)
{
var dialog = new OpenFileDialog
{
DefaultExt = DefaultExt,
Filter = Filter
};
var result = dialog.ShowDialog();
if (result == true)
{
if (FileSelected != null)
{
FileSelected(this, dialog.FileName);
}
if (TextBox != null)
{
TextBox.Text = dialog.FileName;
}
}
}
}
}
So far, so good. I can quickly create a "Browse..." button in XAML. However, I can't get the TextBoxDependency working in the way that I was hoping it would work.
What I want to be able to do is something like this (XAML):
<TextBox x:Name="MyTextBox" />
<extensions:BrowseButton TextBox="MyTextBox" />
However, when I drop that in it says this:
The TypeConverter for "TextBox" does not support converting from a string.
Is there some way to accomplish what I want to do here? To effectively reference another XAML element inside of a XAML element, without having to leave XAML to do it?
Use a binding:
<TextBox x:Name="MyTextBox" />
<extensions:BrowseButton TextBox="{Binding ElementName=MyTextBox}" />
Related
I am trying to create a NavigationViewItem which navigates to two different Views based on a condition.
The UWP app was created using Windows Template Studio and already has the NavigationView and default functions to navigate between pages. I want to have the app check for a condition and then either navigate to one or the other View if the user clicks on the NavigationViewItem. Also the app should keep the colored line, indicating which View it is on, on the one single NavigationViewItem.
This is how my NavigationView currently looks like. I would like to combine the SetTimer and WatchTimer Views. I unfortunately have no idea how the underlying code works as it was all automatically generated and I don't really understand it.
<winui:NavigationView.MenuItems>
<winui:NavigationViewItem x:Uid="Shell_SetTimer" helpers:NavHelper.NavigateTo="views:SetTimerPage" />
<winui:NavigationViewItem x:Uid="Shell_WatchTimer" helpers:NavHelper.NavigateTo="views:WatchTimerPage" />
<winui:NavigationViewItem x:Uid="Shell_History" helpers:NavHelper.NavigateTo="views:HistoryPage" />
</winui:NavigationView.MenuItems>
This is the code behind:
using System;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using ShutdownTimer.Helpers;
using ShutdownTimer.Services;
using Windows.System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Navigation;
using WinUI = Microsoft.UI.Xaml.Controls;
namespace ShutdownTimer.Views
{
public sealed partial class ShellPage : Page, INotifyPropertyChanged
{
private readonly KeyboardAccelerator _altLeftKeyboardAccelerator = BuildKeyboardAccelerator(VirtualKey.Left, VirtualKeyModifiers.Menu);
private readonly KeyboardAccelerator _backKeyboardAccelerator = BuildKeyboardAccelerator(VirtualKey.GoBack);
private bool _isBackEnabled;
private WinUI.NavigationViewItem _selected;
public bool IsBackEnabled
{
get { return _isBackEnabled; }
set { Set(ref _isBackEnabled, value); }
}
public WinUI.NavigationViewItem Selected
{
get { return _selected; }
set { Set(ref _selected, value); }
}
public ShellPage()
{
InitializeComponent();
DataContext = this;
Initialize();
}
private void Initialize()
{
NavigationService.Frame = shellFrame;
NavigationService.NavigationFailed += Frame_NavigationFailed;
NavigationService.Navigated += Frame_Navigated;
navigationView.BackRequested += OnBackRequested;
}
private async void OnLoaded(object sender, RoutedEventArgs e)
{
// Keyboard accelerators are added here to avoid showing 'Alt + left' tooltip on the page.
// More info on tracking issue https://github.com/Microsoft/microsoft-ui-xaml/issues/8
KeyboardAccelerators.Add(_altLeftKeyboardAccelerator);
KeyboardAccelerators.Add(_backKeyboardAccelerator);
await Task.CompletedTask;
}
private void Frame_NavigationFailed(object sender, NavigationFailedEventArgs e)
{
throw e.Exception;
}
private void Frame_Navigated(object sender, NavigationEventArgs e)
{
IsBackEnabled = NavigationService.CanGoBack;
if (e.SourcePageType == typeof(SettingsPage))
{
Selected = navigationView.SettingsItem as WinUI.NavigationViewItem;
return;
}
Selected = navigationView.MenuItems
.OfType<WinUI.NavigationViewItem>()
.FirstOrDefault(menuItem => IsMenuItemForPageType(menuItem, e.SourcePageType));
}
private bool IsMenuItemForPageType(WinUI.NavigationViewItem menuItem, Type sourcePageType)
{
var pageType = menuItem.GetValue(NavHelper.NavigateToProperty) as Type;
return pageType == sourcePageType;
}
private void OnItemInvoked(WinUI.NavigationView sender, WinUI.NavigationViewItemInvokedEventArgs args)
{
if (args.IsSettingsInvoked)
{
NavigationService.Navigate(typeof(SettingsPage));
return;
}
var item = navigationView.MenuItems
.OfType<WinUI.NavigationViewItem>()
.First(menuItem => (string)menuItem.Content == (string)args.InvokedItem);
var pageType = item.GetValue(NavHelper.NavigateToProperty) as Type;
NavigationService.Navigate(pageType);
}
private void OnBackRequested(WinUI.NavigationView sender, WinUI.NavigationViewBackRequestedEventArgs args)
{
NavigationService.GoBack();
}
private static KeyboardAccelerator BuildKeyboardAccelerator(VirtualKey key, VirtualKeyModifiers? modifiers = null)
{
var keyboardAccelerator = new KeyboardAccelerator() { Key = key };
if (modifiers.HasValue)
{
keyboardAccelerator.Modifiers = modifiers.Value;
}
keyboardAccelerator.Invoked += OnKeyboardAcceleratorInvoked;
return keyboardAccelerator;
}
private static void OnKeyboardAcceleratorInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
{
var result = NavigationService.GoBack();
args.Handled = result;
}
public event PropertyChangedEventHandler PropertyChanged;
private void Set<T>(ref T storage, T value, [CallerMemberName]string propertyName = null)
{
if (Equals(storage, value))
{
return;
}
storage = value;
OnPropertyChanged(propertyName);
}
private void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
And the NavHelper
using System;
using Microsoft.UI.Xaml.Controls;
using Windows.UI.Xaml;
namespace ShutdownTimer.Helpers
{
public class NavHelper
{
// This helper class allows to specify the page that will be shown when you click on a NavigationViewItem
//
// Usage in xaml:
// <winui:NavigationViewItem x:Uid="Shell_Main" Icon="Document" helpers:NavHelper.NavigateTo="views:MainPage" />
//
// Usage in code:
// NavHelper.SetNavigateTo(navigationViewItem, typeof(MainPage));
public static Type GetNavigateTo(NavigationViewItem item)
{
return (Type)item.GetValue(NavigateToProperty);
}
public static void SetNavigateTo(NavigationViewItem item, Type value)
{
item.SetValue(NavigateToProperty, value);
}
public static readonly DependencyProperty NavigateToProperty =
DependencyProperty.RegisterAttached("NavigateTo", typeof(Type), typeof(NavHelper), new PropertyMetadata(null));
}
}
Based on the code you post, the navigation happens in the OnItemInvoked event. If you want to have conditions to control the process. That will be the right place. You could set different pagetype in NavigationService.Navigate(pageType) according to different conditions.
Besides, I do not recommend you to use the code directly if you don't understand it. The codes contains all the functions but it also means you don't know why and how it works. It might be a better practice if you could try to implement the function by yourself using native code. After you know more about the navigation, then you could understand these code more easily.
I have been looking for a way to get the HTML out of a WPF WebBrowser control. The two best options I have found are to bind a customer attached property to the property in the application or to build a new control from the WebBrowser control. Considering my level of knowledge and the fact that (as of now I really only need this one time) I chose the first. I even considered breaking MVVM style and using code-behind but I decided not to give up in the binding.
I found several examples on creating the attached property, I finally chose this one, from here Here:
namespace CumminsInvoiceTool.HelperClasses
{
public static class WebBrowserExtentions
{
public static readonly DependencyProperty DocumentProperty =
DependencyProperty.RegisterAttached("Document", typeof(string), typeof(WebBrowserExtentions), new UIPropertyMetadata(null, DocumentPropertyChanged));
public static string GetDocument(DependencyObject element)
{
return (string)element.GetValue(DocumentProperty);
}
public static void SetDocument(DependencyObject element, string value)
{
element.SetValue(DocumentProperty, value);
}
public static void DocumentPropertyChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
WebBrowser browser = target as WebBrowser;
if (browser != null)
{
string document = e.NewValue as string;
browser.NavigateToString(document);
}
}
}
}
I also added the following to the xaml for the WebBrowser control (I have tried both with and without the "Path=" in the xaml:
<WebBrowser local:WebBrowserExtentions.Document="{Binding Path=PageCode}" Source="https://www.cummins-distributors.com/"/>
My View has a tab control one tab has the WebBrowser control and another tab has a textbox. When I click the get code the viewModel runs a function to set property bound to the textbox to the string the attached property of the WebBrowser is bound to. Below is the code of my ViewModel.
namespace CumminsInvoiceTool.ViewModels
{
class ShellViewModel : Screen
{
private string _browserContent;
public string BrowserContent
{
get { return _browserContent; }
set {
_browserContent = value;
NotifyOfPropertyChange(() => BrowserContent);
}
}
private string _pageCode;
public string PageCode
{
get { return _pageCode; }
set {
_pageCode = value;
NotifyOfPropertyChange(() => PageCode);
}
}
public void StartProgressCommand()
{
}
public void GetContent()
{
if (!string.IsNullOrEmpty(PageCode))
{
BrowserContent = PageCode;
}
else
{
MessageBox.Show("There is no cintent to show", "No content Error", MessageBoxButton.OK);
}
}
}
}
The application compiles and runs but when I click "Get Code" I am getting the messagebox for "PageCode" is empty.
When I set a break point at the beginning of the function for the button, the PageCode string is showing "null".
Is this an issue because I am using Caliburn.Micro or am I missing something else?
------- EDIT for comments ----------
The button calls GetContent() in the "ShellViewModel" code above. I know the button is bound and working because the app is showing the custom messagebox I have set up to let me know when "pageCode" is null or empty.
The textbox looks like:
<TextBox x:Name="BrowserContent"/>
This question already has answers here:
DependencyProperty getter/setter not being called
(2 answers)
Closed 7 years ago.
I'm using MVVMLight. This is what I have from an example here.
private ObservableCollection<Inline> _inlineList;
public ObservableCollection<Inline> InlineList
{
get { return _inlineList; }
set { Set(() => InlineList, ref _inlineList, value); }
}
private void SendClicked()
{
InlineList.Add(new Run("This is some bold text") { FontWeight = FontWeights.Bold });
InlineList.Add(new Run("Some more text"));
InlineList.Add(new Run("This is some text") { TextDecorations = TextDecorations.Underline });
}
public class BindableTextBlock : TextBlock
{
public ObservableCollection<Inline> InlineList
{
get { return (ObservableCollection<Inline>)GetValue(InlineListProperty); }
set { SetValue(InlineListProperty, value); }
}
public static readonly DependencyProperty InlineListProperty =
DependencyProperty.Register("InlineList", typeof(ObservableCollection<Inline>), typeof(BindableTextBlock), new UIPropertyMetadata(null, OnPropertyChanged));
private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue == null) return;
var textBlock = (BindableTextBlock)sender;
textBlock.Inlines.AddRange((ObservableCollection<Inline>)e.NewValue);
}
}
<testRobot:BindableTextBlock
Width="Auto" Height="Auto"
InlineList="{Binding InlineList}" >
</testRobot:BindableTextBlock>
The problem is that bound property InlineList never gets updated. I don't see any text I add to the collection ObservableCollection. When I put a break point in OnPropertyChanged method it never gets hit. I know my data context is set correctly as other bound controls work.
What could be the problem?
Ok you only need to add these in your BindableTextBlock With your Original solution. What we do here is we add handler for when collection is changed (meaning new values are added), we only do that when the collection is set. So with the binding that you have in your xaml every change you make on the collection in the VM fires collection changed event in the textblock which in turn just appends values to the Inline.
private void CollectionChangedHandler(object sender, NotifyCollectionChangedEventArgs e)
{
Inlines.AddRange(e.NewItems);
}
private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue == null) return;
var textBlock = (BindableTextBlock)sender;
textBlock.InlineList.CollectionChanged += textBlock.CollectionChangedHandler;
}
BEFORE EDIT for history reasons
Ok I saw what's happening so first the explanation then an example.
So first some basic concepts about wpf:
In order to have your view notified for a change in bound variable in your ViewModel (or whatever that is DataContext at the moment) you have to either RaisePropertyChanged event with the name of the changed property or use something that's doing this somehow :) - like ObservableCollection.
So some use cases:
You have a property with field -> it is common practice to have it like this:
private ICollection<Inline> _inlineList;
public ICollection<Inline> InlineList
{
get
{
return _inlineList;
}
set
{
_inlineList = value;
RaisePropertyChanged("InlineList");
}
}
This ensures that when you set a new value to InlineList the view will be notified
Or in your case what I've used:
private ICollection<Inline> _inlineList;
public ICollection<Inline> InlineList
{
get { return _inlineList; }
set { Set(() => InlineList, ref _inlineList, value); }
}
If you check the description of Set method you'll see that it is setting the value and raising the property (and some more stuff)
You want to have automatic updates and use ObservableCollection -> I use it like this:
private ObservableCollection<ClientFilter> clientFilters;
public IEnumerable<ClientFilter> ClientFilters
{
get
{
if (this.clientFilters == null)
{
this.clientFilters = new ObservableCollection<ClientFilter>();
}
return this.clientFilters;
}
set
{
if (this.clientFilters == null)
{
this.clientFilters = new ObservableCollection<ClientFilter>();
}
SetObservableValues<ClientFilter>(this.clientFilters, value);
}
}
The method SetObservableValues is in my main ViewModel and is doing this:
public static void SetObservableValues<T>(
ObservableCollection<T> observableCollection,
IEnumerable<T> values)
{
if (observableCollection != values)
{
observableCollection.Clear();
foreach (var item in values)
{
observableCollection.Add(item);
}
}
}
This method ensures that if the reference to the obs collection is not the same it will clear the old one and reuse it, because when you bind you bind to the reference at common mistake is to then change the reference itself not the values, which in turn doesn't update anything on the UI and you think binding is broken :)
So If you want it to function normally you just add/remove to the Collection/Enumerable ClientFilters
Now the solution
So I'm not 100% sure what you want to achieve but here's what you could do in order to have your binding working
Your ViewModel
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Documents;
namespace WpfApplication3.ViewModel
{
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
}
private ICollection<Inline> _inlineList;
public ICollection<Inline> InlineList
{
get { return _inlineList; }
set { Set(() => InlineList, ref _inlineList, value); }
}
public RelayCommand SendClicked
{
get
{
return new RelayCommand(() =>
{
InlineList = new List<Inline>
{
new Run("This is some bold text") { FontWeight = FontWeights.Bold },
new Run("Some more text"),
new Run("This is some text") { TextDecorations = TextDecorations.Underline }
};
});
}
}
}
}
Your custom control -> BindableTextBlock
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
namespace WpfApplication3
{
public class BindableTextBlock : TextBlock
{
public ICollection<Inline> InlineList
{
get { return (ICollection<Inline>)GetValue(InlineListProperty); }
set { SetValue(InlineListProperty, value); }
}
public static readonly DependencyProperty InlineListProperty =
DependencyProperty.Register("InlineList", typeof(List<Inline>), typeof(BindableTextBlock), new UIPropertyMetadata(null, OnPropertyChanged));
private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue == null) return;
var textBlock = (BindableTextBlock)sender;
textBlock.Inlines.AddRange((ICollection<Inline>)e.NewValue);
}
}
}
Your XAML
On your Page or Window (depending on platform)
DataContext="{Binding Main, Source={StaticResource Locator}}"
Then inside
<StackPanel>
<Button Command="{Binding SendClicked}">SendClicked</Button>
<local:BindableTextBlock Background="Black" Foreground="AliceBlue"
Width = "Auto" Height="Auto"
InlineList="{Binding InlineList}"
>
</local:BindableTextBlock>
</StackPanel>
All assuming you have your ViewModelLocator from MVVM Light register your view model
using GalaSoft.MvvmLight.Ioc;
using Microsoft.Practices.ServiceLocation;
namespace WpfApplication3.ViewModel
{
public class ViewModelLocator
{
public ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
SimpleIoc.Default.Register<MainViewModel>();
}
public MainViewModel Main
{
get
{
return ServiceLocator.Current.GetInstance<MainViewModel>();
}
}
public static void Cleanup()
{
}
}
}
ALTERNATIVE
Alternatively you could have your command like this:
public RelayCommand SendClicked
{
get
{
return new RelayCommand(() =>
{
_inlineList = new List<Inline>();
InlineList.Add(new Run("This is some bold text") { FontWeight = FontWeights.Bold });
InlineList.Add(new Run("Some more text"));
InlineList.Add(new Run("This is some text") { TextDecorations = TextDecorations.Underline });
RaisePropertyChanged("InlineList");
});
}
}
But you have to use the other option of defining the property as described at the beginning of my post.
You could do it in other ways of course.
Just one more advice -> It is considered bad practice and not in the spirit of MVVM to have UI elements in your view model, so change in architecture is strongly advised in this code IMO.
Post got too long (as usual), if you need aditional explanation please let me know.
Cheers and happy coding ;)
I am using WPF (C#) for the first time and this I've encountered my first "real" design choice. I have a main window and when the user enters some data and presses the "plot" Button, a new window will come up showing a graph.
This graph window I am defining myself with a combination of xaml and the code-behind file. The issue is that 2 parameters this window has is the x axis title and the y axis title. So, these should be "parameters" to making this window.
I am confused by this because I'm using MVVM and I have a "ViewModel" for the window called GraphWindowPresenter and a "View" for the class called GraphWindowView.
At first, I tried to have an xAxis property and a yAxis property in my GraphWindowPresenter but that will not work since I need to "bind" to these values upon construction of the GraphWindowView. Additionally, this approach would require that my GraphWindowPresenter take an xAxis parameter and a yAxis parameter which is problamatic as well since I just create an instance of the class in the xaml of GraphWindowView.
I'm thinking of a possible soltuion that I can just have my GraphWindowView take the xAxis and yAxis parameters but doesn't this violate MVVM? I would rather not do that.
Note: This is similar to this post MVVM: Binding a ViewModel which takes constructor args to a UserControl. But in my scenario it is tricky since I have a parent window and a pop up child window.
Question: What is the best approach to this design issue? What are the "best practices" regarding this scenario?
Possible Answer:
Is this the correct use of dependency properties that you described? Is this a "clean" solution?
private void doGraph()
{
if (log == null) // if a log is not loaded
{
MessageBoxResult mbr = MessageBox.Show("A log file must be " +
"loaded before plotting.",
"Warning",
MessageBoxButton.OK,
MessageBoxImage.Exclamation);
return;
}
// NOW MUST PRESENT GRAPH WINDOW
GraphWindowView gwv = new GraphWindowView();
gwv.xAxis = X_AXIS_VALUE:
gwv.yAxis = Y_AXIS_VALUE;
gwv.Show();
}
And in my GraphWindowView class I have the code:
public partial class GraphWindowView : Window
{
// Using a DependencyProperty as the backing store for yAxis.
public static readonly DependencyProperty yAxisProperty =
DependencyProperty.Register("yAxis", typeof(string), typeof(GraphWindowView));
// Using a DependencyProperty as the backing store for xAxis.
public static readonly DependencyProperty xAxisProperty =
DependencyProperty.Register("xAxis", typeof(string), typeof(GraphWindowView));
public string xAxis
{
get { return (string)GetValue(xAxisProperty); }
set { SetValue(xAxisProperty, value); }
}
public string yAxis
{
get { return (string)GetValue(yAxisProperty); }
set { SetValue(yAxisProperty, value); }
}
public GraphWindowView()
{
InitializeComponent();
}
}
You can you userSetting properties
One my application have same scenario in that i have mainWindow that accept HostAddress,Port value and it will use another window when i click connect so i am using userSetting properties. I am also using MVVM pattern check code snippet below
XAML:
<TextBox Width="120" Canvas.Left="132" Canvas.Top="16" Text="{Binding Path=Server,Mode=TwoWay}"/>
<TextBox Width="120" Canvas.Left="132" Canvas.Top="42" Text="{Binding Path=DisplayPort,Mode=TwoWay}"/>
<TextBox Width="120" Canvas.Left="132" Canvas.Top="69" Text="{Binding Path=CtrlPort,Mode=TwoWay}"/>
<Button Content="Launch" Name="btnLaunch" Command="{Binding Path=appSetting}" Canvas.Left="132" Canvas.Top="100" Width="120" Height="51" Click="btnLaunch_Click" />
VIEWMODE:
public class SettingsViewModel : ViewModelBase
{
private Settings _settings { get; set; }
public SettingsViewModel()
{
appSetting = new RelayCommand(this.AppSettingsCommand);
_settings = ApplicationTest.Properties.Settings.Default;
}
private string _server = Settings.Default.Server;
public string Server
{
get { return this._server; }
set
{
if (this._server != value)
{
this._server = value;
OnPropertyChanged("Server");
}
}
}
private string _displayPort = Settings.Default.DisplayPort;
public string DisplayPort
{
get { return this._displayPort; }
set
{
if (this._displayPort != value)
{
this._displayPort = value;
OnPropertyChanged("DisplayPort");
}
}
}
private string _ctrlPort = Settings.Default.CtrlPort;
public string CtrlPort
{
get { return this._ctrlPort; }
set
{
if (this._ctrlPort != value)
{
this._ctrlPort = value;
OnPropertyChanged("DisplayPort");
}
}
}
public RelayCommand appSetting
{
get;
set;
}
private void AppSettingsCommand()
{
this._settings.Server = this.Server;
this._settings.DisplayPort = this.DisplayPort;
this._settings.CtrlPort = this.CtrlPort;
this._settings.Save();
}
Is there a MVVM way to select text in a textbox? The MVVM framework that I am using is Laurent Bugnion's MVVM Light Toolkit.
Whenever I am trying to directly affect the the View in a "pure" MVVM application (no code-behind in View), I will use Attached Properties to encapsulate whatever effect I am trying to achieve. I will create an interface that defines the actions I wish to take using custom events. I then implement this interface in each ViewModel that will be "running" these commands on the View. Finally, I bind my ViewModel to the attached property in my View definition. The following code shows how to this for SelectAll and a TextBox. This code can be easily expanded to perform just about any action on any component in the View.
My Attached Property and interface definition:
using System.Windows;
using System.Windows.Controls;
using System;
using System.Collections.Generic;
namespace SelectAllSample
{
public static class TextBoxAttach
{
public static readonly DependencyProperty TextBoxControllerProperty = DependencyProperty.RegisterAttached(
"TextBoxController", typeof(ITextBoxController), typeof(TextBoxAttach),
new FrameworkPropertyMetadata(null, OnTextBoxControllerChanged));
public static void SetTextBoxController(UIElement element, ITextBoxController value)
{
element.SetValue(TextBoxControllerProperty, value);
}
public static ITextBoxController GetTextBoxController(UIElement element)
{
return (ITextBoxController)element.GetValue(TextBoxControllerProperty);
}
private static readonly Dictionary<ITextBoxController, TextBox> elements = new Dictionary<ITextBoxController, TextBox>();
private static void OnTextBoxControllerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var element = d as TextBox;
if (element == null)
throw new ArgumentNullException("d");
var oldController = e.OldValue as ITextBoxController;
if (oldController != null)
{
elements.Remove(oldController);
oldController.SelectAll -= SelectAll;
}
var newController = e.NewValue as ITextBoxController;
if (newController != null)
{
elements.Add(newController, element);
newController.SelectAll += SelectAll;
}
}
private static void SelectAll(ITextBoxController sender)
{
TextBox element;
if (!elements.TryGetValue(sender, out element))
throw new ArgumentException("sender");
element.Focus();
element.SelectAll();
}
}
public interface ITextBoxController
{
event SelectAllEventHandler SelectAll;
}
public delegate void SelectAllEventHandler(ITextBoxController sender);
}
My ViewModel definition:
public class MyViewModel : ITextBoxController
{
public MyViewModel()
{
Value = "My Text";
SelectAllCommand = new RelayCommand(p =>
{
if (SelectAll != null)
SelectAll(this);
});
}
public string Value { get; set; }
public RelayCommand SelectAllCommand { get; private set; }
public event SelectAllEventHandler SelectAll;
}
My View definition:
<Window x:Class="SelectAllSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:SelectAllSample"
Title="Window1" Height="150" Width="150">
<x:Code><![CDATA[
public Window1()
{
InitializeComponent();
DataContext = new MyViewModel();
}
]]></x:Code>
<StackPanel>
<TextBox Text="{Binding Value}" loc:TextBoxAttach.TextBoxController="{Binding}" />
<Button Content="Select All" Command="{Binding SelectAllCommand}" />
</StackPanel>
</Window>
Note: Thanks to Josh Smith for RelayCommand (see code in Figure 3 on this page). It is used in MyViewModel in this example (and just about all my MVVM code).
find a good introduction to attached properties here:
http://www.codeproject.com/KB/WPF/AttachedBehaviors.aspx