I have got a question to wpf community here.
I am kind of not able to understand Routing Tunnel Events. In my application, I have got a window which contains a tool bar.
Window also contains usercontrols. There are some controls in Tool bar like view which are used to Hide / unhide usercontrols (Views) like in Visual Studio.
I have custom routed tunnel event in windows control. I raise custom event when a button is clicked on toolbar (hide / unhide). I need to hide a expander in child usercontrol (which has a name like "Expander 1") when button is clicked.
Can some one tell me how can I capture the raised event in the child user control?
Thanks.
Code window :
public partial class MainWindow : Window
{
private static readonly RoutedEvent HideShowMitigationEvent;
static MainWindow()
{
HideShowMitigationEvent = EventManager.RegisterRoutedEvent("HideShowMitigation",
RoutingStrategy.Tunnel, typeof(RoutedEventHandler), typeof(MainWindow));
}
public MainWindow()
{
InitializeComponent();
}
// The Standard .Net optional event wrapper
// This is required if we want to register the event handler in XAML
public event RoutedEventHandler HideShowMitigation
{
add { AddHandler(HideShowMitigationEvent, value); }
remove { RemoveHandler(HideShowMitigationEvent, value); }
}
// Raise the event. overidden from UIElement
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
// RaiseEvent(new RoutedEventArgs(HideShowMitigationEvent, this));
}
public static ExploredRisks _rootName { get; set; }
public MainWindow(GeneralTree<string> rawTreeData, Excel.Worksheet sheet,Excel.Workbook Wb)
{
//prepares the visual tree for other views
PrepareVisualTree visualTree = new PrepareVisualTree(rawTreeData, sheet);
_rootName = visualTree.getVisualTree();
var l_vm = new MainViewModel();
l_vm.Load(_rootName);
TreeListViewMultiColumned view = new TreeListViewMultiColumned( RiskViewModel.CreateTestModel(visualTree.getVisualTree()),sheet,Wb);
base.DataContext = l_vm;
InitializeComponent();
}
private void UIPanel_Loaded(object sender, RoutedEventArgs e)
{
}
private void RibbonCheckBox_Checked(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(HideShowMitigationEvent, this));
}
private void SimpleClickEventHandlingCode(object sender, RoutedEventArgs e)
{
//Expander exp = ((MainWindow)(e.OriginalSource)).RiskProperties.MitigationArea;
RoutedEventArgs args = new RoutedEventArgs();
args.RoutedEvent = HideShowMitigationEvent;
RaiseEvent(args);
}
}
}
Window Xaml:
<Window>
<Ribbon x:Name="RibbonWin" SelectedIndex="0">
<RibbonTab Header="Views" KeyTip="H">
<!-- Home group-->
<RibbonGroup x:Name="ViewsGroup" Header="Views">
<RibbonCheckBox Label="Mitigation" IsChecked="{Binding IsChecked, Mode=TwoWay}" Checked="RibbonCheckBox_Checked" PreviewMouseDown="SimpleClickEventHandlingCode"/>
<RibbonCheckBox Label="Properties" IsChecked="{Binding IsChecked, Mode=TwoWay}" Checked="RibbonCheckBox_Checked" />
</RibbonGroup>
</RibbonTab>
</Ribbon>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<UI:TreeListViewMultiColumned x:Name="RiskProperties" Grid.Column="0" />
</Grid>
</Window>
I think I have to to clarify on WPF Routed Events before I suggest a solution:
In WPF there is a new concept of Routed Events. Routed Events are Events that are passed along the logical tree.
Example:
Lets look at what happens when you click a button on your UI.
First, you will get a PreviewLeftMouseButtonDown event that occurs on the MainWindow and is then passed down the element tree from parent to child until it reaches the button that has been clicked.
-> This process (from parent to child) is called Tunneling
Second, you will get a LeftMouseButtonDown event that occurs on the button and is passed up the element tree until it reaches the MainWindow.
-> This process (from child to parent) is called Bubbling
As far as I understand you want to open the expander on the click of the button.
IMHO using routed events for this is not the appropriate approach.
I think you can solve your use case with a little XAML. Here is what I suggest:
You use a ToggleButton in the Toolbar (this ensures that the user can
see the state of the button, e.g. pressed or not pressed.)
You use DataBinding to bind the ToggleButtons IsChecked Property to the
Expanders IsExpanded property.
Check the following (highly simplified) sample:
<Grid>
<StackPanel>
<ToggleButton x:Name="openExpanderBtn" Width="100" Height="30" Margin="20" Content="Click to Open" />
<Expander Width="150" Height="200" IsExpanded="{Binding ElementName=openExpanderBtn, Path=IsChecked}" >
<Expander.Header>
This is my Header
</Expander.Header>
This is my Body
</Expander>
</StackPanel>
Remark: It just came to my mind that this only works if the UserControl is under your control. If this is the case: fine, else I will describe another solution.
Rgds MM
Related
Perhaps I'm going about this the wrong way, but my layout is in a way where I have multiple Expanders in a TabControl and I want to add an "expand all" button.
Now logically this button should be inside the tab as it would control the elements in the tab so they ought to be grouped together. Visually however this would be a waste of space as I got a lot of empty space on the Tab Header bar (not sure what the terminology is, the row with the tabheaders).
So what I'm trying to achieve is adding a button outside the content of the tab. The canvas element seems to be what I need to use and it's working as far as its repositioning the element but it gets cut off. This is much easier to explain with a picture so
(if you look hard you can see where the button is as the header covering it is slightly translucent)
Now I can position it where I'd like it to be by moving it outside the TabItem but then I would have to write code to see which tab is focussed and hide it when it's not "Current" that is focussed. That to me sounds like the wrong way to do it as the only thing I want to do is move a button which is a 'view' type of thing.
My MainWindow.axaml:
<TabControl Grid.Row="1" Grid.Column="0" VerticalAlignment="Stretch">
<TabItem Header="Current" ZIndex="1">
<ScrollViewer Classes="CrawlersInAction">
<StackPanel>
<Canvas>
<Button Canvas.Right="10" Canvas.Top="-20" ZIndex="5">Expand All</Button>
</Canvas>
<!-- My very long template code for rendering the expanders -->
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
I do have a background in HTML/CSS so I thought Zindex would the trick and tried applying it in various places without any luck.
PS: I'm using Avalonia instead of WPF but it's pretty much a cross-platform clone, so any WPF know-how probably carries over 1:1.
If you think about it, this functionality lives in the ViewModel at the same "level" as the Tab Control.
<Grid>
<TabControl Items="{Binding MyTabViewModels}" SelectedItem={Binding SelectedTab} />
</Grid>
An Instance of MyTabViewModel has a collection on it:
public ObservableCollection<MyCollectionType> Items
The item class MyCollectionType has an IsExpanded property ...
public bool IsExpanded {get;set;}
Bound to your Expander control IsExpanded property.
Shove your button into the XAML
<Grid>
<TabControl Items="{Binding MyTabViewModels}" />
<Button Commmand={Binding ExpandAllCommand} />
</Grid>
Now on your base ViewModel your ICommand can do something like:
public void ExpandAllCommandExecuted()
{
foreach(var vm in SelectedTab.Items)
{
vm.IsExpanded = true;
}
}
Good luck, this is all pseudocode but illustrates a potential pattern.
The problem seems to have originated from placing my <canvas> control inside the <scrollviewer> control. I've placed it outside it whilst trying many things it seems and it works as I wanted it to. The buttons are visible rendering on top of the tabbar (TabStrip).
My XAML is now:
<TabControl Grid.Row="1" Grid.Column="0" VerticalAlignment="Stretch">
<TabItem Header="Current">
<StackPanel>
<Canvas>
<StackPanel Orientation="Horizontal" Canvas.Right="0" Canvas.Bottom="10" Spacing="5">
<Button Command="{Binding CollapseAll}" IsEnabled="{Binding !AllAreCollapsed}">Collapse All</Button>
<Button Command="{Binding ExpandAll}" IsEnabled="{Binding !AllAreExpanded}">Expand All</Button>
</StackPanel>
</Canvas>
<ScrollViewer Classes="CrawlersInAction">
<StackPanel>
<ItemsControl Name="itemscontrol" Items="{Binding SiteInfos}" VerticalAlignment="Stretch">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Expander ExpandDirection="Down" IsExpanded="{Binding IsExpanded, Mode=TwoWay}" VerticalAlignment="Stretch">
<!-- Ommited my very long template code -->
</Expander>
<DataTemplate>
<ItemsControl.ItemTemplate>
<ItemsControl>
</StackPanel>
</ScrollViewer>
</StackPanel>
</TabItem>
</TabControl>
Codewise I ended up adding a "IsExpanded" property to my SiteInfo class that is used as the base for the expanders IsExpanded property and kept in sync by making it a two way binding as per the XAML above. The code on SiteInfo is:
public class SiteInfo : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public static readonly bool StartIsExpanded = true;
private bool _isExpanded = StartIsExpanded;
public bool IsExpanded
{
get { return _isExpanded; }
set
{
if (value != IsExpanded)
{
_isExpanded = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsExpanded)));
}
}
}
When I create my SiteInfo objects in MainWindowViewModel I subscribe to the events (siteInfo.PropertyChanged += SiteInfo_PropertyChanged;). When the event is received and it would change if my collapse or expand all button should be disabled it sends it own PropertyChangedEvent which then enables/disabled the control.
public class MainWindowViewModel : ViewModelBase, INotifyPropertyChanged
{
public new event PropertyChangedEventHandler? PropertyChanged;
public ObservableCollection<SiteInfo> SiteInfos { get; }
= new ObservableCollection<SiteInfo>();
//Change SiteInfo.StartExpanded if you want this changed.
private bool _allAreExpanded = SiteInfo.StartIsExpanded;
public bool AllAreExpanded
{
get => _allAreExpanded;
set
{
if (_allAreExpanded != value)
{
_allAreExpanded = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AllAreExpanded)));
}
}
}
//Change SiteInfo.StartExpanded if you want this changed.
private bool _allAreCollapsed = !SiteInfo.StartIsExpanded;
public bool AllAreCollapsed {
get { return _allAreCollapsed; }
set {
if (_allAreCollapsed != value)
{
_allAreCollapsed = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AllAreCollapsed)));
}
}
}
private void SiteInfo_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if(e.PropertyName == nameof(siteInfo.IsExpanded))
{
AllAreCollapsed = AreAllCollapsed();
AllAreExpanded = AreAllExpanded();
}
}
public bool AreAllCollapsed()
{
return !SiteInfos.Any<SiteInfo>( siteInfo => siteInfo.IsExpanded );
}
public bool AreAllExpanded()
{
return !SiteInfos.Any<SiteInfo>( siteInfo => siteInfo.IsCollapsed);
}
public void CollapseAll()
{
foreach(SiteInfo siteInfo in SiteInfos)
{
siteInfo.IsExpanded = false;
}
}
public void ExpandAll()
{
foreach (SiteInfo siteInfo in SiteInfos)
{
siteInfo.IsExpanded = true;
}
}
}
Figured I'd add the rest of my code in case anyone Googles this up and wants to do something similar.
So now when my program loads and everything is set to the default expanded true Expand All is disabled, Collapse all is enabled. Changing one expander to collapsed status will have both buttons enabled and collapsing all expanders will disable the Collapse All button.
I have created custome window (titlebar, min/max/ext buttons, own border for window manipulation and lots of styles and triggers).
There are 5 methods defined (which i would like to override):
From window markup:
SourceInitialized="Window_SourceInitialized"
Closing="Window_Closing"
From Titlebar buttons:
Exit_Click()
Max_Click()
Min_Click()
And at last I have DockPanel
<DockPanel Name="ClientArea"/>
In which I want to put my content
I have tried to add content from code:
BaseWindow editInterfaceWindow = new BaseWindow() { Owner = this };
editInterfaceWindow.DataContext = new EditInterface();
editInterfaceWindow.ShowDialog();
But this way some bindings stoped working and inside editInterfaceWindow I cant create another window this way because of Owner = this. There are also some problems with InitializeComponent() in constructor.
And ListView inside EditInterface UserControl <ListView Name="LBAvaliable" ItemsSource="{Binding AvaliableFaces, UpdateSourceTrigger=PropertyChanged}"> is not visible in code as LBAvaliable.
I have used that window few times, filling ClientArea with content by hand.
How should I create other windows, so that I can just inherit it or just define binding? So my XAML for every single window does not take ~1000 lines of code.
In the past I've used MVVMCross Framework and we never had to worry about this ourselves. Though this is not the best, here's an idea on what you can do.
Create a view model that can be overridden for your user control.
Set data templates.
Programmatically change the view model for your user control's main content and let data templates do the work for the UI.
View Model: Pre-defined 3 button actions ready for you to set/override.
public class MainUCViewModel : ViewModelBase
{
private Action<object> btnACommand;
private Action<object> btnBCommand;
private Action<object> btnCCommand;
private object ccVM;
public ViewModelBase CCVM
{
get { return this.ccVM; }
set
{
this.ccVM = value;
OnPropertyChanged(); // Notify View
}
}
public MainUCViewModel()
{
}
public RelayCommand BtnACommand
{
get { return new RelayCommand(btnACommand); }
}
public RelayCommand BtnBCommand
{
get { return new RelayCommand(btnBCommand); }
}
public RelayCommand BtnCCommand
{
get { return new RelayCommand(btnCCommand); }
}
public void SetBtnACommand(Action<object> action)
{
this.btnACommand = action;
}
public void SetBtnBCommand(Action<object> action)
{
this.btnBCommand = action;
}
public void SetBtnCCommand(Action<object> action)
{
this.btnCCommand = action;
}
}
View:
<UserControl x:Class="WpfApplication1.Views.UserControls.MainUC"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="500" d:DesignWidth="750">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="45" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<StackPanel Orientation="Horizontal">
<Button Command="{Binding BtnACommand}" Width="100">
<TextBlock>A</TextBlock>
</Button>
<Rectangle Width="15" />
<Button Command="{Binding BtnBCommand}" Width="100">
<TextBlock>B</TextBlock>
</Button>
<Rectangle Width="15" />
<Button Command="{Binding BtnCCommand}" Width="100">
<TextBlock>C</TextBlock>
</Button>
</StackPanel>
</Grid>
<Grid Grid.Row="1">
<ContentControl x:Name="CCMain" Content="{Binding CCVM}"/>
</Grid>
</Grid>
</UserControl>
Look at Thinking with MVVM: Data Templates + ContentControl. Simply define the data template for your view model.
<Window.Resources>
<DataTemplate DataType="{x:Type ViewModel:GeneralSettingsViewModel}">
<View:GeneralSettingsView/>
</DataTemplate
<DataTemplate DataType="{x:Type ViewModel:AdvancedSettingsViewModel}">
<View:AdvancedSettingsView/>
</DataTemplate>
</Window.Resources>
What I’m saying here is that GeneralSettingsViewModel should be
rendered using a GeneralSettingsView. That’s exactly what we need !
Because the Views are created using a DataTemplate, we do not need to
setup the DataContext, it will be automatically registered to the
templated object, the ViewModel.
There are two main approaches to your problem:
Inherited windows
Configurable windows
For approach 1, design your window and make the methods overrideable:
In base window xaml, assign the handlers and everything you want:
<Window x:Class="WpfTests.MainWindow"
...
SourceInitialized="Window_SourceInitialized">
In base window, define the handlers as protected virtual (or abstract, if you like to enforce their implementation)
public partial class MainWindow : Window
{
// ...
protected virtual void Window_SourceInitialized(object sender, EventArgs e)
{
}
// ...
}
Create derived windows
public class ExWindow : MainWindow
{
protected override void Window_SourceInitialized(object sender, EventArgs e)
{
// specialized code here
}
}
Change App.xaml to use Startup instead of StartupUri
<Application x:Class="WpfTests.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Startup="Application_Startup">
And manually create your first window, chosing one of the inherited window classes
public partial class App : Application
{
private void Application_Startup(object sender, StartupEventArgs e)
{
var window = new ExWindow();
window.Show();
}
}
The second approach - configurable windows - follows the same principle as a good user control design: The window/control properties are controlled by the creator instead of being controlled by the window/control itself.
So, instead of defining some event handler within the window code, just leave this exercise to the user, who hopefully knows what the window should do:
public partial class MainWindow : Window
{
// I don't care for SourceInitialized (also remove it from XAML)
}
In App.xaml or wherever a window is created:
public partial class App : Application
{
private void Application_Startup(object sender, StartupEventArgs e)
{
var window = new MainWindow();
window.SourceInitialized += window_SourceInitialized;
window.Show();
}
void window_SourceInitialized(object sender, EventArgs e)
{
var window = sender as MainWindow;
// I know how to handle this event for this window instance
}
}
Following Josh Smith example on mvvm workspaces (customers view), I have a mainwindow and a mainwindowviewmodel which contains an ObservableCollection of "ChatTabViewModel":
internal class FriendsListViewModel : ObservableObject
{
#region bound properties
private ICollectionView viewfriends;
private ObservableCollection<ChatTabViewModel> _chatTab;
...
#endregion
}
I have an area dedicated to this collection in the xaml like that :
<ContentControl Grid.Column="0" Grid.Row="0" Grid.RowSpan="2" Content="{Binding Path=ChatTabs}" ContentTemplate="{StaticResource ChatTabsTemplate}" />
And in my resources dictionary:
<DataTemplate DataType="{x:Type vm:ChatTabViewModel}">
<View:ChatTabView />
</DataTemplate>
<DataTemplate x:Key="ClosableTabItemTemplate">
<DockPanel>
<Button
Command="{Binding Path=CloseCommand}"
Content="X"
Cursor="Hand"
DockPanel.Dock="Right"
Focusable="False"
FontFamily="Courier"
FontSize="9"
FontWeight="Bold"
Margin="0,1,0,0"
Padding="0"
VerticalContentAlignment="Bottom"
Width="16" Height="16"
/>
<ContentPresenter
Content="{Binding Path=Caption, Mode=OneWay}"
VerticalAlignment="Center">
</ContentPresenter>
</DockPanel>
</DataTemplate>
<DataTemplate x:Key="ChatTabsTemplate">
<TabControl
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding}"
ItemTemplate="{StaticResource ClosableTabItemTemplate}"
Margin="4"/>
</DataTemplate>
On user event I add a new ChattabViewModel in my collection and the view related to it appears in the main window.
But when I tried to add an attached property on a scrollbar in the ChattabView, this property will attach only on the first ChattabViewModel instance, the other tabs won't be bound to the attached property. Here's the ChattabView XAML:
<ScrollViewer VerticalScrollBarVisibility="Auto" Grid.Row="0">
<ItemsControl ItemsSource="{Binding Messages}" View:ItemsControlBehavior.ScrollOnNewItem="True">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox IsReadOnly="True" TextWrapping="Wrap" Text="{Binding Path=DataContext, RelativeSource={RelativeSource Self}}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
and the code of the attached property:
namespace GtalkOntre.View
{
/// <summary>
/// Util class to scroll down when a new message is added.
/// </summary>
/// <remarks>attached property called ScrollOnNewItem that when set to true hooks into the INotifyCollectionChanged events of the itemscontrol items source and upon detecting a new item, scrolls the scrollbar to it.</remarks>
public class ItemsControlBehavior
{
static Dictionary<ItemsControl, Capture> Associations = new Dictionary<ItemsControl, Capture>();
public static bool GetScrollOnNewItem(DependencyObject obj)
{
return (bool)obj.GetValue(ScrollOnNewItemProperty);
}
public static void SetScrollOnNewItem(DependencyObject obj, bool value)
{
obj.SetValue(ScrollOnNewItemProperty, value);
}
public static DependencyProperty ScrollOnNewItemProperty =
DependencyProperty .RegisterAttached(
"ScrollOnNewItem",
typeof(bool),
typeof(ItemsControlBehavior),
new UIPropertyMetadata(false, OnScrollOnNewItemChanged));
public static void OnScrollOnNewItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var mycontrol = d as ItemsControl;
if (mycontrol == null) return;
bool newValue = (bool)e.NewValue;
if (newValue)
{
mycontrol.Loaded += MyControl_Loaded;
mycontrol.Unloaded += MyControl_Unloaded;
}
else
{
mycontrol.Loaded -= MyControl_Loaded;
mycontrol.Unloaded -= MyControl_Unloaded;
if (Associations.ContainsKey(mycontrol))
Associations[mycontrol].Dispose();
}
}
static void MyControl_Unloaded(object sender, RoutedEventArgs e)
{
var mycontrol = (ItemsControl)sender;
Associations[mycontrol].Dispose();
mycontrol.Unloaded -= MyControl_Unloaded;
}
static void MyControl_Loaded(object sender, RoutedEventArgs e)
{
var mycontrol = (ItemsControl)sender;
var incc = mycontrol.Items as INotifyCollectionChanged;
if (incc == null) return;
mycontrol.Loaded -= MyControl_Loaded;
Associations[mycontrol] = new Capture(mycontrol);
}
class Capture : IDisposable
{
public ItemsControl mycontrol { get; set; }
public INotifyCollectionChanged incc { get; set; }
public Capture(ItemsControl mycontrol)
{
this.mycontrol = mycontrol;
incc = mycontrol.ItemsSource as INotifyCollectionChanged;
incc.CollectionChanged +=incc_CollectionChanged;
}
void incc_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
ScrollViewer sv = mycontrol.Parent as ScrollViewer;
sv.ScrollToBottom();
}
}
public void Dispose()
{
incc.CollectionChanged -= incc_CollectionChanged;
}
}
}
}
So why is the attached property only bound once, on the first "chattabview" occurence of the chattabviewmodel collection? and therefore, working only on the first chattabviewmodel.
When I close them all, the attached property will unbind itself on the last instance of chattabviewmodel, and when I add a new first chattabviewmodel, the property will bind correctly. So it triggers only on the first instance and last instance of the "chattabviewmodel" collection of mainwindowviewmodel.
After a week of searching, I'm a little desperate now...
So far my hypothesis is : the problem might be related to the way I set the view to my viewmodel in dictionary resources. The view might be shared and the first scrollbar only might react. I tried to add an x:Shared = false attribute on the DataTemplate tag but it didn't change anything.
Are you sure there are different instances of your ChatTabView being created?
I believe WPF's TabControl re-uses the existing template if it's the same instead of creating a new one, and simply replaces the DataContext behind it.
So it would only create one copy of your ChatTabView and switching tabs is replacing the DataContext behind the ChatTabView to a different item in the collection.
You haven't shown us ChatTabsTemplate, so I can only assume it contains a TabControl. If so, that explains the behavior you're seeing. The TabControl lazily loads its child tab items, so only the current view will be initialized, and hence have the attached property applied to it. When you switch tabs, however, you should see the same attached property firing. Is that not the case?
As for your hunch, it's not quite right. The DataTemplate is being shared, but the DataTemplate is used to create distinct instances of its contents, which are not being shared.
I have just started learning MVVM. I've made the application from scratch by following this MVVM tutorial (I highly recommend it to all MVVM beginners out there). Basically, what I have created so far is a couple of text boxes where user adds his or her data, a button to save that data which subsequently populates the ListBox with all entries made.
Here's where I got stuck: I want to be able to double-click on a ListBoxItem and to trigger a command that I have created and added to my ViewModel. I don't know how to finish the XAML side, i.e. I don't know how to bind that command to the ListBox(Item).
Here's XAML:
...
<ListBox
Name="EntriesListBox"
Width="228"
Height="208"
Margin="138,12,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
ItemsSource="{Binding Entries}" />
...
Here's ViewModel:
public class MainWindowViewModel : DependencyObject
{
...
public IEntriesProvider Entries
{
get { return entries; }
}
private IEntriesProvider entries;
public OpenEntryCommand OpenEntryCmd { get; set; }
public MainWindowViewModel(IEntriesProvider source)
{
this.entries = source;
...
this.OpenEntryCmd = new OpenEntryCommand(this);
}
...
}
And finally, here's the OpenEntryCommand that I want to be executed once the user double-clicks the item in the EntriesListBox:
public class OpenEntryCommand : ICommand
{
private MainWindowViewModel viewModel;
public OpenEntryCommand(MainWindowViewModel viewModel)
{
this.viewModel = viewModel;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public bool CanExecute(object parameter)
{
return parameter is Entry;
}
public void Execute(object parameter)
{
string messageFormat = "Subject: {0}\nStart: {1}\nEnd: {2}";
Entry entry = parameter as Entry;
string message = string.Format(messageFormat,
entry.Subject,
entry.StartDate.ToShortDateString(),
entry.EndDate.ToShortDateString());
MessageBox.Show(message, "Appointment");
}
}
Please help, I'd appreciate it.
Unfortunately, only ButtonBase derived controls have the possibility for binding ICommand objects to their Command properties (for the Click event).
However, you can use an API provided by Blend to map an event (like in your case MouseDoubleClick on the ListBox) to an ICommand object.
<ListBox>
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseDoubleClick">
<i:InvokeCommandAction Command="{Binding YourCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ListBox>
You'll have to define: xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" and have a reference to System.Windows.Interactivity.dll.
-- EDIT --
This is part of WPF4, but u can use Microsoft.Windows.Interactivity if you're not using WPF4. This dll is from Blend SDK, which doesn't require Blend, from here:
http://www.microsoft.com/downloads/en/details.aspx?FamilyID=f1ae9a30-4928-411d-970b-e682ab179e17&displaylang=en
Update: I found something that should help you. check this link on MVVM Light Toolkit which contains a walkthrough on how to do this, along with a link to the needed libraries. MVVM Light Toolkit is a very interesting framework for applying MVVM with Silverlight, WPF, and WP7.
Hope this helps :)
This is made tricky because of the DoubleClick event. There are a few ways to do this:
Handle the double-click event in code behind, and then manually invoke a command/method on your ViewModel
Use an attached behavior to route the DoubleClick event to your Command
Use a Blend Behavior to map the DoubleClick event to your command
2 and 3 might be more pure, but frankly, 1 is easier, less complex, and not the worst thing in the world. For a one-off case, I'd probably use approach #1.
Now, if you changed your requirements to use, say, a hyperlink on each item, it would be easier. Start out by naming the root element in your XAML - e.g., for a Window:
<Window .... Name="This">
Now, in the DataTemplate for your ListBox items, use something like this:
<ListBox ...>
<ListBox.ItemTemplate>
<DataTemplate>
<Hyperlink
Command="{Binding ElementName=This, Path=DataContext.OpenEntryCmd}"
Text="{Binding Path=Name}"
/>
The ElementName binding lets you resolve the OpenEntryCmd from the context of your ViewModel, rather than the specific data item.
EDIT: I wrote this post as an inexperienced WPF developer, nowadays I'd either use a framework that provides event to command binding, or simply use a button and restyle it. Of course for maximum flexibility this is maybe better.
I find the best way to do this is to create a simple user control wrapper for my content, with dependency properties for the command and parameter.
The reason I did this was due to the Button not bubbling the click event to my ListBox which prevented it from selecting the ListBoxItem.
CommandControl.xaml.cs:
public partial class CommandControl : UserControl
{
public CommandControl()
{
MouseLeftButtonDown += OnMouseLeftButtonDown;
InitializeComponent();
}
private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs mouseButtonEventArgs)
{
if (Command != null)
{
if (Command.CanExecute(CommandParameter))
{
Command.Execute(CommandParameter);
}
}
}
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register("Command", typeof(ICommand),
typeof(CommandControl),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.None));
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
public static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.Register("CommandParameter", typeof(object),
typeof(CommandControl),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.None));
public object CommandParameter
{
get { return (object)GetValue(CommandParameterProperty); }
set { SetValue(CommandParameterProperty, value); }
}
}
CommandControl.xaml:
<UserControl x:Class="WpfApp.UserControls.CommandControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
Background="Transparent">
</UserControl>
Usage:
<ListBoxItem>
<uc:CommandControl Command="{Binding LoadPageCommand}"
CommandParameter="{Binding HomePageViewModel}">
<TextBlock Text="Home" Margin="0,0,0,5" VerticalAlignment="Center"
Foreground="White" FontSize="24" />
</uc:CommandControl>
</ListBoxItem>
The Content can be whatever, and when the control is clicked, it will execute the command.
EDIT: Added Background="Transparent" to UserControl to enable click events on the entire area of the control.
This is a bit of a hack, but it works well and allows you to use commands and avoid code behind. This also has the added benefit of not triggering the command when you double-click (or whatever your trigger is) in the empty ScrollView area assuming your ListBoxItems don't fill the entire container.
Basically, just create a DataTemplate for your ListBox that is composed of a TextBlock and bind the width of the TextBlock to the width of the ListBox, set the margins and padding to 0, and disable horizontal scrolling (because the TextBlock will bleed beyond the visible bounds of the ScrollView triggering the horizontal scroll bar otherwise). The only bug I've found is that the command won't fire if the user clicks precisely on the border of the ListBoxItem, which I can live with.
Here is an example:
<ListBox
x:Name="listBox"
Width="400"
Height="150"
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
ItemsSource="{Binding ItemsSourceProperty}"
SelectedItem="{Binding SelectedItemProperty}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Padding="0"
Margin="0"
Text="{Binding DisplayTextProperty}"
Width="{Binding ElementName=listBox, Path=Width}">
<TextBlock.InputBindings>
<MouseBinding
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBox}}, Path=DataContext.SelectProjectCommand}"
Gesture="LeftDoubleClick" />
</TextBlock.InputBindings>
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I recently needed to trigger an ICommand upon double clicking a ListBoxItem as well.
Personally, I don't like the DataTemplate method as it is binding to the content inside the ListBoxItem container, and not the container itself. I've opted to use an Attached Property to assign an InputBinding on the container. It takes a little more elbow grease, but it works well.
First, we need to create an attached property class. I've created mine a little more generically towards any class that derives from FrameworkElement, just in case I run into this again with a different visual.
public class FrameworkElementAttachedProperties : DependencyObject
{
public static readonly DependencyProperty DoubleClickProperty = DependencyProperty.RegisterAttached("DoubleClick", typeof(InputBinding),
typeof(FrameworkElementAttachedProperties), new PropertyMetadata(null, OnDoubleClickChanged));
public static void SetDoubleClick(FrameworkElement element, InputBinding value)
{
element.SetValue(DoubleClickProperty, value);
}
public static InputBinding GetDoubleClick(FrameworkElement element)
{
return (InputBinding)element.GetValue(DoubleClickProperty);
}
private static void OnDoubleClickChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
FrameworkElement element = obj as FrameworkElement;
/// Potentially throw an exception if an object is not a FrameworkElement (is null).
if(e.NewValue != null)
{
element.InputBindings.Add(e.NewValue as InputBinding);
}
if(e.OldValue != null)
{
element.InputBindings.Remove(e.OldValue as InputBinding);
}
}
}
Then the final step is to override the base container style for the ListBoxItem.
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}"
BasedOn="{StaticResource ListBoxItem}">
<Setter Property="local:FrameworkElementAttachedProperties.DoubleClick">
<Setter.Value>
<MouseBinding Command="{Binding OnListBoxItemDoubleClickCommand}"
MouseAction="LeftDoubleClick"/>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
Now, anytime a ListBoxItem is double clicked, it will fire our OnListBoxItemDoubleClickCommand.
If you're looking for a nice simple solution that uses interactions instead of mucking about with user controls, code behind, input bindings, custom attached properties, etc.
And you want something that works at the ListBoxItem level, i.e. not ListBox level as per the (incorrectly) accepted solution.
Then here's a snippet for a simple 'button like' click action..
<ListBox>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Background="Transparent">
<!-- insert your visuals here -->
<b:Interaction.Triggers>
<b:EventTrigger EventName="MouseUp">
<b:InvokeCommandAction Command="{Binding YourCommand}" />
</b:EventTrigger>
</b:Interaction.Triggers>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Note, background="Transparent" is required to ensure the entire Grid is clickable and not just the contents inside.
I have a custom control that contains two elements that can be clicked (a button and a check box). I'd like to be able to put events in the XAML for each of these events.
i.e.
<Control OnButtonClick="SomeEvent" OnCheckBoxClick="SomeOtherEvent" />
I have no idea how to bind events like that. Any pointers?
The following is the content of the user control:
<Style TargetType="{x:Type local:DeleteCheckBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:DeleteCheckBox}">
<Grid>
<Label Height="25" BorderBrush="LightGray" BorderThickness="1" Padding="0" DockPanel.Dock="Top" FlowDirection="RightToLeft" Visibility="Hidden">
<Button x:Name="ImageButton1" Background="Transparent" Padding="0" BorderBrush="Transparent" Height="11" Width="11" Click="--Bind Function Here--" />
</Label>
<CheckBox Content="A0000" Click="--Bind Function Here--" IsChecked="True" Margin="0,10,10,0" VerticalContentAlignment="Center"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
You need to route the events from your children to your top element.
In the code-behind of your top element, define the RoutedEvents you need. Then, in the constructor, subscribe to your children required events, and in the handlers, throw a new top-element event corresponding to your handled child event with the same args.
Example
Note: Look for custom routed events on google. In this example, you still need to copy the button event arguments (if you need them, to get access to current button state, etc) to the touted event.
public class MyCustomControl : UserControl {
// Custom routed event
public static readonly RoutedEvent ButtonClickEvent = EventManager.RegisterRoutedEvent(
"ButtonClick", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(MyCustomControl));
// Custom CLR event associated to the routed event
public event RoutedEventHandler ButtonClick {
add { AddHandler(ButtonClickEvent, value); }
remove { RemoveHandler(ButtonClickEvent, value); }
}
// Constructor. Subscribe to the event and route it !
public MyCustomControl() {
theButton.Click += (s, e) => {
RaiseButtonClickEvent(e);
};
}
// Router for the button click event
private void RaiseButtonClickEvent(RoutedEventArgs args) {
// you need to find a way to copy args to newArgs (I never tried to do this, google it)
RoutedEventArgs newArgs = new RoutedEventArgs(MyCustomControl.ButtonClickEvent);
RaiseEvent(newArgs);
}
}