I have a listbox that is bound to a list of custom objects. I can get the listbox items to display correctly using the ListBox.ItemTemplate in xaml. The custom objects for the listbox are all of the same base class outlined below.
public class HomeViewMenuItem : UIElement
{
private Uri _uri;
private IRegionManager _manager;
public HomeViewMenuItem(string text, Uri uri, IRegionManager manager)
{
this.PreviewMouseDown += HomeViewMenuItem_PreviewMouseDown;
this.PreviewKeyDown += HomeViewMenuItem_PreviewKeyDown;
_manager = manager;
Text = text;
_uri = uri;
ClickCommand = new DelegateCommand(this.Click, this.CanClick);
}
void HomeViewMenuItem_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if (e.Key == System.Windows.Input.Key.Enter)
{
e.Handled = true;
this.ClickCommand.Execute();
}
}
void HomeViewMenuItem_PreviewMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
e.Handled = true;
this.ClickCommand.Execute();
}
private void Click()
{
_manager.Regions[RegionNames.MainRegion].RequestNavigate(_uri);
}
private bool CanClick()
{
return true;
}
public DelegateCommand ClickCommand { get; set; }
public string Text { get; set; }
}
The problem I am having is the HomeViewMenuItem_PreviewKeyDown method is not getting called. I believe this is because the method is getting called on the ListBoxItem itself first and getting handled there. I was able to verify this by obtaining a reference to the ListBoxItem object through listBox.ItemContainerGenerator.ContainerFromIndex(0) after the ItemContainerGenerator status changes to ContainersGenerated and adding an event handler there. This event handler correctly fired. Normally this would be an ok solution on a small project but I plan on having more listboxes with the same sort of functionality and would like to have a simpler/better solution. Is there a way that I can get my base class previewkeydown method to work?
The only solution I could think of is to have the base class inherit from ListBoxItem instead of UIElement then get the ListBox to create my items instead of ListBoxItems. But I dont think that is really possible without creating my own ListBox implementation.
You seem to be somewhat confused. In WPF, we create data items and declare DataTemplates to define what those items should look like in the UI. Our data items do not extend UI classes. If you have to handle the PreviewKeyDown event, then attach a handler to the UI element in the DataTemplate instead:
<DataTemplate>
<Grid PreviewKeyDown="HomeViewMenuItem_PreviewKeyDown">
...
</Grid>
</DataTemplate>
Related
I've been going back and forth over the pros and cons of the two following approaches to Events from custom controls. My debate basically revolves around how much "logic" should be placed within a custom (not user) control and to best get events into a viewmodel.
The "control", DataGridAnnotationControl, resides within an adorner to my data grid. The goal here is to respond to the user selecting an item from a combobox displayed within the custom control.
The first example, Example #1, uses a pretty standard custom event in the DataGridAnnotationControl
which is then mapped by way of the adorner to the target AppointmentEditor (viewmodel). My biggest complaint with this is the obvious dependency to the (AppointmentEditor) from the adorner to achieve proper event routing.
♦ Example #1:
♦ CustomControl DataGridAnnotationControl
public override void OnApplyTemplate()
{
......
_cboLastName.SelectionChanged += _cboLastName_SelectionChanged;
}
private void _cboLastName_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
RaiseSelectionChanged();
}
public event Action SelectionChanged;
public void RaiseSelectionChanged()
{
SelectionChanged?.Invoke();
}
♦ Adorner DataGridAnnotationAdorner
public DataGridAnnotationAdorner(DataGrid adornedDataGrid)
: base(adornedDataGrid)
{
......
Control = new DataGridAnnotationControl();
this.SelectionChanged += ((AppointmentEditor)adornedDataGrid.DataContext).SelectionChanged; <--This requires a reference to Patient_Registration.Editors. THIS IS FORCING
A DEPENDENCY ON THE PATIENT_REGISTRATION PROJECT.
}
public event Action SelectionChanged
{
add { Control.SelectionChanged += value; }
remove { Control.SelectionChanged -= value; }
}
♦ AppointmentEditor
public void SelectionChanged()
{
throw new NotImplementedException();
}
Example #2 This example uses pretty standard event routing up to the mainwindow from which an event aggregator is being used to hit the AppointmentEditor as a subscriber to the event. My biggest complaint here is all the additional code needed (over Example #1). In addition, it seems like a complicating factor to climb the visual tree just to jump into the one viewmodel designed to support this customcontrol.
Example #2:
♦ CustomControl DataGridAnnotationControl
public override void OnApplyTemplate()
{
.....
_cboLastName.SelectionChanged += _cboLastName_SelectionChanged;
}
private void _cboLastName_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
RaisePatientNameSelectionChangedEvent();
}
public static readonly RoutedEvent PatientNameSelectionChangedEvent = EventManager.RegisterRoutedEvent(
"PatientNameSelectionChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(DataGridAnnotationControl));
// Provide CLR accessors for the event
public event RoutedEventHandler PatientNameSelectionChanged
{
add { AddHandler(PatientNameSelectionChangedEvent, value); }
remove { RemoveHandler(PatientNameSelectionChangedEvent, value); }
}
protected virtual void RaisePatientNameSelectionChangedEvent()
{
RoutedEventArgs args = new RoutedEventArgs(DataGridAnnotationControl.PatientNameSelectionChangedEvent);
RaiseEvent(args);
}
♦ public partial class MainWindow : Window
{
public MainWindow(IMainWindowViewModel mainWindowViewModel, EventAggregator eventAggregator)
{
InitializeComponent();
EventAggregator = eventAggregator;
DataContext = mainWindowViewModel;
....
AddHandler(DataGridAnnotationControl.PatientNameSelectionChangedEvent, new RoutedEventHandler(PatientNameSelectionChangedHandler));
}
private void PatientNameSelectionChangedHandler(object sender, RoutedEventArgs e)
{
EventAggregator.PublishEvent( new PatientNameSelected() );
}
}
♦ public class AppointmentEditor : INotifyPropertyChanged, ISubscriber<PatientNameSelected>
public void OnEventHandlerAsync(PatientNameSelected e)
{
throw new NotImplementedException();
}
Is there a preferred way of doing this?
TIA
Ideally, your custom control should have no knowledge of your view-models.
Using MVVM, you would bind an event in your custom control to a command in your view-model.
I author and maintain tons of custom controls that are used by a lot of other teams. I always expose an associated ICommand with any event to make it easy for MVVM users to use my controls in the easiest way possible.
I'm diving into WPF, coming from a Winforms background where I used groupboxes to show and hide "panels" depending on what menu options were clicked. This was a bit of a nightmare at designtime having multiple overlapping groupboxes.
Does WPF solve this problem? Is the groupbox still the way to go? Or are there better solutions?
The WPF GroupBox has a property on it called 'Visibility' (inherited from UIElement) which can be controlled in a View Model via binding.
This Xaml fragment shows a GroupBox's visibility being bound to a property called 'MyGroupBoxVisibility'...
<Grid>
<GroupBox Header="This and that" Visibility="{Binding MyGroupBoxVisibility}" Background="Plum"/>
</Grid>
A sample View Model which contains the 'MyGroupBoxVisibility' is...
public class ViewModel : INotifyPropertyChanged
{
public ViewModel()
{
HideTheGroupBox();
}
private void ShowTheGroupBox()
{
MyGroupBoxVisibility = Visibility.Visible;
}
private void HideTheGroupBox()
{
MyGroupBoxVisibility = Visibility.Collapsed;
}
private Visibility _myGroupBoxVisibility;
public Visibility MyGroupBoxVisibility
{
[DebuggerStepThrough]
get { return _myGroupBoxVisibility; }
[DebuggerStepThrough]
set
{
if (value != _myGroupBoxVisibility)
{
_myGroupBoxVisibility = value;
OnPropertyChanged("MyGroupBoxVisibility");
}
}
}
#region INotifyPropertyChanged Implementation
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string name)
{
var handler = System.Threading.Interlocked.CompareExchange(ref PropertyChanged, null, null);
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
#endregion
}
This sample has two methods in it which toggle the visibility back and forth. You can use this technique to transfer your WinForms stuff into WPF and also keep it within MVVM.
ETA: Note that it's vital to understand the difference between Collapsed and Hidden visibilities. Please see http://msdn.microsoft.com/en-us/library/system.windows.visibility(v=vs.110).aspx for an explanation
I need to know when a WPF Datagrid has been sorted by the user. Why is there no Sorted event? I can only find a Sorting event.
I also investigated the CollectionView and ListCollectionView that is exposing the objects to the View, without any luck.
I am quite surprised as this should come out of the box.
Any ideas?
You can still subscribe to the DataGrid Sorting Event:
<local:CustomDataGrid x:Name="datagrid" Sorting="datagrid_Sorted;"/>
but to make sure that your actions happen after the sorting is done use Dispatcher :
private void DataGrid_Sorting(object sender, DataGridSortingEventArgs e)
{
this.Dispatcher.BeginInvoke((Action)delegate()
{
//runs after sorting is done
}, null);
}
This way, there's no need of a custom Datagrid class.
I've taken an example from MSDN documentation and adjusted it to raise a Sorted event when the Sorting event is done.
public class CustomDataGrid : DataGrid
{
// Create a custom routed event by first registering a RoutedEventID
// This event uses the bubbling routing strategy
public static readonly RoutedEvent SortedEvent = EventManager.RegisterRoutedEvent(
"Sorted", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(CustomDataGrid));
// Provide CLR accessors for the event
public event RoutedEventHandler Sorted
{
add { AddHandler(SortedEvent, value); }
remove { RemoveHandler(SortedEvent, value); }
}
// This method raises the Sorted event
void RaiseSortedEvent()
{
RoutedEventArgs newEventArgs = new RoutedEventArgs(CustomDataGrid.SortedEvent);
RaiseEvent(newEventArgs);
}
protected override void OnSorting(DataGridSortingEventArgs eventArgs)
{
base.OnSorting(eventArgs);
RaiseSortedEvent();
}
}
Then you can use it either in codebehind.
datagrid.Sorted += new RoutedEventHandler(datagrid_Sorted);
or in XAML
<local:CustomDataGrid x:Name="datagrid" Sorted="datagrid_Sorted"/>
And here the method that will get triggered when the datagrid finishing sorting:
private void datagrid_Sorted(object sender, RoutedEventArgs args)
{
var datagrid = (CustomDataGrid)sender;
var sortedItems = datagrid.Items;
}
datagrid has "Sorting" event, subscribe to it!
XAML:
<DataGrid ItemsSource="{Binding YourItems}" AutoGenerateColumns="True" anUserSortColumns="True"
Sorting="DataGrid_Sorting"/>
.cs code:
private void DataGrid_Sorting(object sender, System.Windows.Controls.DataGridSortingEventArgs e)
{
Console.WriteLine(string.Format("sorting grid by '{0}' column in {1} order", e.Column.SortMemberPath, e.Column.SortDirection));
}
I'm new to MVVM and trying to figure out how to close a ChildWindow with the traditional Cancel button using MVVM Light Toolkit.
In my ChildWindow (StoreDetail.xaml), I have :
<Button x:Name="CancelButton" Content="Cancel" Command="{Binding CancelCommand}" />
In my ViewModel (ViewModelStoreDetail.cs), I have :
public ICommand CancelCommand { get; private set; }
public ViewModelStoreDetail()
{
CancelCommand = new RelayCommand(CancelEval);
}
private void CancelEval()
{
//Not sure if Messenger is the way to go here...
//Messenger.Default.Send<string>("ClosePostEventChildWindow", "ClosePostEventChildWindow");
}
private DelegateCommand _cancelCommand;
public ICommand CancelCommand
{
get
{
if (_cancelCommand == null)
_cancelCommand = new DelegateCommand(CloseWindow);
return _cancelCommand;
}
}
private void CloseWindow()
{
Application.Current.Windows[Application.Current.Windows.Count - 1].Close();
}
If you displayed your child window by calling ShowDialog(), then you can simply set the IsCancel property of your button control to "True".
<Button Content="Cancel" IsCancel="True" />
It becomes the same as clicking the X button on the window, or pressing ESC on the keyboard.
Have a look at this articleon MSDN. About half way down there is an approach on how to do this. Basically it uses either uses a WorkspaceViewModel or you implements an interface that exposes and event RequestClose
You then inside the Window's DataContext (if you are setting the ViewModel to it) you can attach to the event.
This is an excerpt from the article (Figure 7). You can adjust it to suit your needs.
// In App.xaml.cs
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
MainWindow window = new MainWindow();
// Create the ViewModel to which
// the main window binds.
string path = "Data/customers.xml";
var viewModel = new MainWindowViewModel(path);
// When the ViewModel asks to be closed,
// close the window.
viewModel.RequestClose += delegate
{
window.Close();
};
// Allow all controls in the window to
// bind to the ViewModel by setting the
// DataContext, which propagates down
// the element tree.
window.DataContext = viewModel;
window.Show();
}
It's been a while since I've used WPF and MVVMLight but yes I think I'd use the messanger to send the cancel event.
In MVVM Light Toolkit the best what you can do is to use Messenger to interact with the View.
Simply register close method in the View (typically in the code behind file) and then send request to close a window when you need it.
We have implemented a NO-CODE BEHIND functionality. See if it helps.
EDIT: Here is there Stackoverflow discussion
Here are some ways to accomplish it.
Send message to your childwindow and set DialogueResult to false on childwindow code-behind.
Make property of DialogueResult and Bind it with childwindow Dialoue CLR property, set it on CancelEval method of CancelCommand.
Create object of Childwindow and set DialogueResult false on CancelEval.
Kind of late to the party but I thought I'd add my input. Borrowing from user841960's answer:
public RelayCommand CancelCommand
{
get;
private set;
}
Then:
SaveSettings = new RelayCommand(() => CloseWindow());
Then:
private void CloseWindow()
{
Application.Current.Windows[Application.Current.Windows.Count - 1].Close();
}
It's a bit cleaner than using an ICommand and works just as well.
So, to sum it all up, the example class would look like so:
public class ChildViewModel
{
public RelayCommand CancelCommand
{
get;
private set;
}
public ChildViewModel()
{
SaveSettings = new RelayCommand(() => CloseWindow());
}
private void CloseWindow()
{
Application.Current.Windows[Application.Current.Windows.Count - 1].Close();
}
}
I've got a UserControl with an ItemsSource property. As the base UserControl class does not implement ItemsSource, I had to create my own dependency property like this:
#region ItemsSource Dependency Property
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(MonthViewControl),
new PropertyMetadata(OnItemsSourceChanged));
static void OnItemsSourceChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
(obj as MonthViewControl).OnItemsSourceChanged(e);
}
private void OnItemsSourceChanged(DependencyPropertyChangedEventArgs e)
{
RefreshLayout();
}
public IEnumerable ItemsSource
{
get
{
return (base.GetValue(ItemsSourceProperty) as IEnumerable);
}
set
{
base.SetValue(ItemsSourceProperty, value);
}
}
#endregion
Now in my ViewModel I have an Events property which is an ICollectionView of EventItem items like so:
private ObservableCollection<Controls.EventCalendar.EventItem> eventItems;
private CollectionViewSource events;
public System.ComponentModel.ICollectionView Events
{
get
{
if (events == null)
{
events = new CollectionViewSource();
events.Source = eventItems;
}
return events.View;
}
}
The issue I'm facing is that in my View, when I bind to the Events property, and I add an Item to eventItems, the UserControl won't fire the ItemsSourceChanged event and hence not update the UI.
For the sake of testing I added a simple listbox to the view which also binds to the Events property. That works like a charm. Updates to eventItems observableCollection are reflected in the ListBox.
I'm figuring it has something to do with my ItemsSource dependency property. Maybe I would need to use a Custom Control which inherits form ItemsControl instead of a UserControl?
To help you understand my problem: I'm trying to create a calendar like control which shows events/agenda entries (similar to Google Calendar). It works like a charm. The UI is updated when the control is resized. The only thing that's left is the automagical update once the ItemsSource changes.
Hope someone can help.
EDIT: The moment I posted I realized that the event can't be fired as the ItemsSource property does not change. It is the underlying collection that changes. However, I'm not how to handle that. What do I need to implement to make this work. Just a hint would be enough. I don't need every implementation details.
Opening the PresentationFramework.dll within Reflector and looking at System.Windows.Controls.ItemsControl showed the following:
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(IEnumerable),
typeof(ItemsControl), new FrameworkPropertyMetadata(null,
new PropertyChangedCallback(ItemsControl.OnItemsSourceChanged)));
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ItemsControl control = (ItemsControl) d;
IEnumerable oldValue = (IEnumerable) e.OldValue;
IEnumerable newValue = (IEnumerable) e.NewValue;
ItemValueStorageField.ClearValue(d);
if ((e.NewValue == null) && !BindingOperations.IsDataBound(d, ItemsSourceProperty))
{
control.Items.ClearItemsSource();
}
else
{
control.Items.SetItemsSource(newValue);
}
control.OnItemsSourceChanged(oldValue, newValue);
}
Not knowing what RefreshLayout does my hunch is that it has something to do with the way the ObservableCollection<T> is being wrapped as the above code is oblivious to what the concrete collection type is and it would therefore be handled by the type being wrapped; in this case an ObservableCollection<T> Try modifying your property as seen below to return the default view and adjust your ItemsSource property to be more akin to the above code from the framework and work backwards from there.
private ObservableCollection<Controls.EventCalendar.EventItem> eventItems;
private ICollectionview eventsView;
public System.ComponentModel.ICollectionView Events
{
get
{
if (eventsView == null)
eventsView = CollectionViewSource.GetDefaultView(eventItems);
return eventsView;
}
}