I was faced with the next misunderstanding.
Preamble:
I have wpf application with next essential UI parts: RadioButtons and some control that use dropdown based on Popup (in combobox manner). According to some logic every radiobutton hook PreviewMouseDown event and do some calculations.
In the next scenario,
User opens popup (do not select something, popup just staying open)
User click on radiobutton
PreviewMouseDown will not be fired for radiobutton as expected (because of Popup feature).
And my aim is firing PreviewMouseDown for RadioButton despite of one.
Attempts to solve:
Fast and dirty solution is: hook PreviewMouseDown for Popup and re-fire PreviewMouseDown event with new source if required, using radiobutton as source. New source can be obtained via MouseButtonEventArgs.MouseDevice.DirectlyOver. The next piece of code do that (event is re-fired only if Popup "eat" PreviewMouseDown for outer click):
private static void GrantedPopupPreviewMouseDown(object sender, MouseButtonEventArgs e)
{
var popup = sender as Popup;
if(popup == null)
return;
var realSource = e.MouseDevice.DirectlyOver as FrameworkElement;
if(realSource == null || !realSource.IsLoaded)
return;
var parent = LayoutTreeHelper.GetParent<Popup>(realSource);
if(parent == null || !Equals(parent, popup ))
{
e.Handled = true;
var args = new MouseButtonEventArgs(e.MouseDevice,
e.Timestamp,
e.ChangedButton)
{
RoutedEvent = UIElement.PreviewMouseDownEvent,
Source = e.MouseDevice.DirectlyOver,
};
realSource.RaiseEvent(args);
}
}
This works well when I'm attaching that handler to Popup.PreviewMouseDown directly via Behavior and do not work (PreviewMouseDown isn't fired for radiobutton) if I'm attaching one via EventManager.RegisterClassHandler (aim is to avoid attaching behavior to every Popup that can occure on page with these radiobuttons):
EventManager.RegisterClassHandler(
typeof (Popup),
PreviewMouseDownEvent,
new MouseButtonEventHandler(GrantedPopupPreviewMouseDown));
Debugger showed that e.MouseDevice.DirectlyOver (see code above) is Popup, not Radiobutton (as it is was when I've attached handler via Behavior)!
Question:
How and whyMouseButtonEventArgs can be different for the same action, if eventhandler attached in two different ways?
Can someone explaing this behavior?
Thanks a lot.
The combo box is provided as a way for users to select from a group of options, and you likely want to do that. But it also has other contracts. It says that the user should be focused on this and only this task. But that is not your situation. You want to show the options, have them hide able, and allow the user to do other things while they are shown.
I think instead of combo boxes you want some other control. My suggestion is to use an expander that contains a listbox. Given:
class NotificationObject : INotifyPropertyChanged
{
public void RaisePropertyChanged(string name)
{
if(PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
class ComboEntry : NotificationObject
{
public string Name { get; private set; }
private string _option = "Off";
public string Option
{
get { return _option; }
set { _option = value; RaisePropertyChanged("Option"); }
}
public ComboEntry()
{
Name = Guid.NewGuid().ToString();
}
}
class MyDataContext : NotificationObject
{
public ObservableCollection<ComboEntry> Entries { get; private set; }
private ComboEntry _selectedEntry;
public ComboEntry SelectedEntry
{
get { return _selectedEntry; }
set { _selectedEntry = value; RaisePropertyChanged("SelectedEntry"); }
}
public MyDataContext()
{
Entries = new ObservableCollection<ComboEntry>
{
new ComboEntry(),
new ComboEntry(),
new ComboEntry()
};
SelectedEntry = Entries.FirstOrDefault();
}
public void SetOption(string value)
{
Entries
.ToList()
.ForEach(entry => entry.Option = value);
}
}
I think you want the following XAML:
<Window x:Class="RadioInCombo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:RadioInCombo"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<local:MyDataContext x:Key="myDataContext" />
<DataTemplate x:Key="ComboEntryTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" />
<Border Width="5" />
<TextBlock Text="{Binding Option}" />
</StackPanel>
</DataTemplate>
</Window.Resources>
<StackPanel DataContext="{StaticResource myDataContext}">
<RadioButton x:Name="OnButton"
Content="On"
PreviewMouseDown="OnButton_PreviewMouseDown" />
<RadioButton x:Name="OffButton"
Content="Off"
PreviewMouseDown="OffButton_PreviewMouseDown" />
<Expander Header="{Binding SelectedEntry}"
HeaderTemplate="{StaticResource ComboEntryTemplate}">
<ListBox ItemsSource="{Binding Entries}"
ItemTemplate="{StaticResource ComboEntryTemplate}" />
</Expander>
</StackPanel>
</Window>
And the following code-behind:
private MyDataContext GetMyDataContext()
{
var candidate = FindResource("myDataContext") as MyDataContext;
if (candidate == null) throw new ApplicationException("Could not locate the myDataContext object");
return candidate;
}
private void OnButton_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
GetMyDataContext().SetOption("On");
}
private void OffButton_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
GetMyDataContext().SetOption("Off");
}
Related
I have a ViewModel, from which I show a Window that should fade out. It fades only first time, and then It stops.
public class MessageBoxViewModel
{
private MessageBoxView _message;
private MessageBoxResult _result = MessageBoxResult.No;
public MessageBoxViewModel()
{
//...creating commands...
}
private void Window_Closing(object sender, CancelEventArgs e)
{
//Close window with fade out animation
_message.Closing -= Window_Closing;
e.Cancel = true;
var animation = new DoubleAnimation
{
To=0,
Duration=TimeSpan.FromSeconds(1),
FillBehavior = FillBehavior.Stop
};
animation.Completed += (s, a) => { _message.Close(); };
_message.BeginAnimation(UIElement.OpacityProperty, animation);
}
public MessageBoxResult Show(string Text, string Title)
{
//...setting properties which View is bound to
_message = new MessageBoxView
{
DataContext = this
};
_message.Closing += Window_Closing;
_message.ShowDialog();
return _result;
}
}
And this is how I call messagebox in different ViewModel:
class SomeViewMNodel : INotifyPropertyChanged
{
private MessageBoxViewModel _message = new MessageBoxViewModel();
public SomeViewModel()
{
//....
}
private void ShowMessages(object parameter) //Command on click of some button
{
_message.Show("Hey I'm fading.", "Fade out"); //Fading is succesfully done
_message.Show("Hey I'm fading second time.", "Fade out"); //Fading doesn't work anymore
}
}
I have tried to stop an animation as suggested here, but that doesn't seem to work. Neither does Opacity property actually change - a simple check with var j = _message.GetAnimationBaseValue(UIElement.OpacityProperty) ==> allways shows value of 1, in animation.Completed or after inicializing new Window.
I've figured that animation works If I don't use _message variable, but instead declare a new instance of ViewModel, e.g. var win = new MessageBoxViewModel(). But I'm using this custom MessageBox for all errors & notifications in many ViewModels, so I would like to use only _message variable If possible (I would make it global).
Without MVVM and re-initiliazing instance of Window I can make animation working everytime, but how can I animate Window in MVVM properly?
I don't see the point in making the Window instance global and reuse the Window instance. You should generally avoid creating global (static) instances.
Creating a small Window instance from time to time is quite affordable. Instantiation costs are unnoticeable in this case.
Anyway, if you want to reuse a Window instance, you are not allowed to close it. Closing a Window disposes its unmanaged resources.
If you want to use Window.Close(), you have to override the virtual Window.OnClosing method (or listen to the Window.Closing event) and cancel closing and instead set the window's visibility to Visibilty.Collapsed:
private void Window_Closing(object sender, CancelEventArgs e)
{
//Close window with fade out animation
_message.Closing -= Window_Closing;
e.Cancel = true;
var animation = new DoubleAnimation
{
To=0,
Duration=TimeSpan.FromSeconds(1),
FillBehavior = FillBehavior.Stop
};
animation.Completed += (s, a) => _message.Visibility = Visibility.Collapsed; ;
_message.BeginAnimation(UIElement.OpacityProperty, animation);
}
But as some have noticed already, this implementation is violating the MVVM pattern. You are not allowed to introduce a coupling between the view and the view model. The goal of MVVM is to remove this exact coupling.
The view model is not allowed to handle any UI components.
The following example shows how implement a dialog infrastructure that complies with the MVVM pattern.
The example consists of four simple steps
Create a DialogViewModel
Create the Dialog, which will extend Window. It will use a DataTemplate to show its content based on the DialogViewModel (or subtypes) in the Dialog.DataContext
Create a public property e.g., DialogViewModel in your view model that needs to show the dialog.
Let the parent control e.g. MainWindow actually show the Dialog.
Example implementation
Implement a view model that serves as the data source for the actual dialog window:
DialogViewModel.cs
This view model defines an AcceptCommand and a CancelCommand which can be bound to corresponding dialog buttons.
When one of the commands is executed a CloseRequested event is raised.
The constructor takes a delegate which serves as a callback that is invoked when the dialog was closed.
public class DialogViewModel : INotifyPropertyChanged
{
public DialogViewModel(string dialogCaption, string message, Action<DialogViewModel> dialogClosedHandler)
{
this.DialogCaption = dialogCaption;
this.Message = message;
this.DialogClosedHandler = dialogClosedHandler;
}
public void HandleResult() => this.DialogClosedHandler.Invoke(this);
private string dialogCaption;
public string DialogCaption
{
get => this.dialogCaption;
set
{
this.dialogCaption = value;
OnPropertyChanged();
}
}
private string message;
public string Message
{
get => this.message;
set
{
this.message = value;
OnPropertyChanged();
}
}
public ICommand AcceptCommand => new RelayCommand(param => this.IsAccepted = true);
public ICommand CancelCommand => new RelayCommand(param => this.IsAccepted = false);
private bool isAccepted;
public bool IsAccepted
{
get => this.isAccepted;
set
{
this.isAccepted = value;
OnPropertyChanged();
OnCloseRequested();
}
}
public event EventHandler<DialogEventArgs> CloseRequested;
public event PropertyChangedEventHandler PropertyChanged;
private Action<DialogViewModel> DialogClosedHandler { get; }
protected virtual void OnCloseRequested()
{
this.CloseRequested?.Invoke(this, new DialogEventArgs(this));
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Implement the dialog window, which will show its content based on the DialogViewModel using a DataTemplate. To create different types of specialized dialogs, simply ceate a specialized dialog view model and a corresponding DataTemplate.
Every eventual dialog animation is also implemented in this class using XAML and EventTrigger, which triggers on a DialogClosed routed event.
The Dialog will listen to the DialogViewModel.CloseRequested event to close itself. Since you wished to reuse the Dialog instance, the Dialog intercepts the invocation of Close() to collapse itself. This behavior can be enabled using the constructor.
After closing itself, the Dialog sets the DialogEventArgs.Handled property to true, which will trigger the invocation of the dialog closed callback (which was registered with the DialogViewModel), so that the calling view model, that showed the dialog, can continue to execute:
Dialog.xaml.cs
public partial class Dialog : Window
{
#region DialogClosedRoutedEvent
public static readonly RoutedEvent DialogClosedRoutedEvent = EventManager.RegisterRoutedEvent(
"DialogClosed",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(Dialog));
public event RoutedEventHandler DialogClosed
{
add => AddHandler(Dialog.DialogClosedRoutedEvent, value);
remove => RemoveHandler(Dialog.DialogClosedRoutedEvent, value);
}
#endregion
private bool IsReuseEnabled { get; }
public Dialog(bool isReuseEnabled = false)
{
InitializeComponent();
this.IsReuseEnabled = isReuseEnabled;
this.DataContextChanged += OnDialogViewModelChanged;
}
public Dialog(DialogViewModel dialogViewModel) : this()
{
this.DataContext = dialogViewModel;
}
private void OnDialogViewModelChanged(object sender, DependencyPropertyChangedEventArgs e)
{
HandleDialogNewViewModel(e.OldValue as DialogViewModel, e.NewValue as DialogViewModel);
}
private void HandleDialogNewViewModel(DialogViewModel oldDialogViewModel, DialogViewModel newDialogViewModel)
{
if (oldDialogViewModel != null)
{
oldDialogViewModel.CloseRequested -= CloseDialog;
}
if (newDialogViewModel != null)
{
newDialogViewModel.CloseRequested += CloseDialog;
}
}
private void CloseDialog(object sender, DialogEventArgs e)
{
Close();
e.Handled = true;
}
#region Overrides of Window
protected override void OnClosing(CancelEventArgs e)
{
if (!this.IsReuseEnabled)
{
return;
}
e.Cancel = true;
Dispatcher.InvokeAsync(
() => RaiseEvent(new RoutedEventArgs(Dialog.DialogClosedRoutedEvent, this)),
DispatcherPriority.Background);
base.OnClosing(e);
}
#endregion
private void DialogClosedAnimation_OnCompleted(object? sender, EventArgs e)
{
this.Visibility = Visibility.Collapsed;
}
}
Dialog.xaml
To customize the layout, edit the DataTemplate:
<Window x:Class="Dialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Height="450"
Width="800"
Title="{Binding DialogCaption}">
<Window.Resources>
<!--
To create more specialized dialogs,
create a dedicated DataTemplate for each dialog view model type.
-->
<DataTemplate DataType="{x:Type local:DialogViewModel}">
<StackPanel>
<TextBlock Text="{Binding Message}"/>
<StackPanel Orientation="Horizontal">
<Button Content="Ok" Command="{Binding AcceptCommand}" />
<Button Content="Cancel" IsDefault="True" IsCancel="True" Command="{Binding CancelCommand}" />
</StackPanel>
</StackPanel>
</DataTemplate>
</Window.Resources>
<!-- Animations triggered by the DialogClosed event -->
<Window.Triggers>
<EventTrigger RoutedEvent="local:Dialog.DialogClosed">
<BeginStoryboard>
<Storyboard Completed="DialogClosedAnimation_OnCompleted">
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:1" FillBehavior="Stop"/>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" FillBehavior="Stop">
<DiscreteObjectKeyFrame KeyTime="0:0:1" Value="{x:Static Visibility.Hidden}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Window.Triggers>
<Grid>
<ContentPresenter Content="{Binding}"/>
</Grid>
</Window>
To trigger the displaying of the Dialog let the view model create the DialogViewModel and assign it to a public property.
ViewModel.cs
public class ViewModel : INotifyPropertyChanged
{
public void SaveToFile(object data, string filePath)
{
// Check if file exists (pseudo)
if (string.IsNullOrWhiteSpace(filePath))
{
// Show the dialog
this.DialogViewModel = new DialogViewModel("File Exists Dialog", "File exists. Replace file?", OnDialogResultAvailable);
}
else
{
Save(data, filePath);
}
}
public void Save(object data, string filePath)
{
// Write data to file
}
private void OnDialogResultAvailable(DialogViewModel dialogViewModel)
{
if (dialogViewModel.IsAccepted)
{
// User has accepted to overwrite file
Save(data, filePath);
}
}
private DialogViewModel dialogViewModel;
public DialogViewModel DialogViewModel
{
get => this.dialogViewModel;
set
{
this.dialogViewModel = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
To actually show the dialog, let the parent Window (e.g., MainWindow) listen to the property changes of ViewModel.DialogViewModel e.g., by setting up a Binding:
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public static readonly DependencyProperty CurrentDialogViewModelProperty = DependencyProperty.Register(
"CurrentDialogViewModel",
typeof(DialogViewModel),
typeof(MainWindow),
new PropertyMetadata(default(DialogViewModel), MainWindow.OnDialogViewModelChanged));
public DialogViewModel CurrentDialogViewModel
{
get => (DialogViewModel) GetValue(MainWindow.CurrentDialogViewModelProperty);
set => SetValue(MainWindow.CurrentDialogViewModelProperty, value);
}
private static void OnDialogViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue == null)
{
return;
}
(d as MainWindow).ShowDialog(e.NewValue as DialogViewModel);
}
private void ShowDialog(DialogViewModel dialogViewModel)
{
this.Dialog.DataContext = dialogViewModel;
this.Dialog.ShowDialog();
// Alternative recommended approach:
// var dialog = new Dialog(dialogViewModel);
// dialog.ShowDialog();
}
private Dialog Dialog { get; set; }
public MainWindow()
{
InitializeComponent();
// Create a reusable dial instance (not recommended)
this.Dialog = new Dialog(true);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
(this.DataContext as ViewModel).SaveToFile(null, string.Empty);
}
}
MainWindow.xaml
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
<Window.Resources>
<Style TargetType="local:MainWindow">
<-- Observe view model DialogViewModel property using data binding -->
<Setter Property="CurrentDialogViewModel" Value="{Binding DialogViewModel}" />
</Style>
</Window.Resources>
<Button Content="Show Dialog" Click="Button_Click" />
</Window>
Remarks
You can improve reusability by moving the code implemented in MainWindow to an attached behavior.
On a popup window I have a checkbox.IsChecked bound to a model, but I want to check its state from the xaml code behind when the window is displayed. When checking the checkbox by name in the code behind from the windows loaded event it is not set yet. There are some UI specific things that I need to do and that is why I need to know what the checkboxes value is when the window opens and i cannot perform this from the model that the checkbox.IsChecked is bound to.
The property on the model is set long before the popup window is opened, so it is not an issue of the binding not being there. I figured that once the Loaded event fires the window would be ready to use bindings and all, but this does not seem to be the case.
Xaml:
<RefinedRibbonControls:RefinedRibbonGroup Header="Show Graphs">
<StackPanel x:Name="panelVisibilities">
<CheckBox Content="Show/hide" x:Name="myCheckBox"
IsChecked="{Binding Path=Processor.Model.IsItemVisible}"
Click="GraphVisibilityClickEvent"
HorizontalAlignment="Left"/>
...etc
Property on model:
public bool IsItemVisible
{
get { return _isItemVisible ; }
set
{
if (_isItemVisible != value)
{
_isItemVisible = value;
_propertyChanger.FirePropertyChanged(this, m => m.IsItemVisible);
}
}
}
Event in Xaml codebehind:
private void WindowLoadedEvent(object sender, RoutedEventArgs e)
{
if(myCheckBox.IsChecked.Value)
{
// Do UI Related Stuff
}
}
The binding works fine and the values show up when the window is displayed, the problem is I cannot get the value of the binding in the window loaded event.
Edit: Possible solution I have found but I am not sure if its the best way.
I called the following method from the constructor on the xaml code behind.
private void SetupInitialVisibility()
{
//Fire after everything is loaded.
Dispatcher.BeginInvoke(DispatcherPriority.ContextIdle, new Action(() =>
{
IEnumerable<CheckBox> elements = this.panelVisibilities.Children.OfType<CheckBox>().ToList();
foreach (CheckBox checkBox in elements)
{
if (checkBox.IsChecked != null && checkBox.IsChecked.Value == false)
{
//Do work
}
}
}));
}
found at: https://stackoverflow.com/a/1746975/1253746
Data binding is not done synchronously, but it is delayed. Check this msdn page on dispatcher priorities. It is done at a lower priority than normal window messages, but before rendering.
You could invoke a method on yourself with a lower priority than is defined for databinding, in this method you should be able to safely read the data bound value.
I would still find this ugly. I'd rather subscribe directly to PropertyChanged and check for this property, or even better, rewrite your "UI related code" as a data binding.
P.S. If you start consuming events, be sure to unsubscribe, or you might get memory leaks.
DataBinding should precede the Loaded event I think.
When and how do you set your DataContext? And you are positive that the viewmodel property is already set?
The following works, try to align your code with this if possible.
Xaml:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
<Grid>
<CheckBox x:Name="myCheckBox" IsChecked="{Binding IsItemVisible}" />
</Grid>
</Window>
Code Behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.Loaded += MainWindow_Loaded;
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
if (myCheckBox.IsChecked.Value)
{
//...
}
}
}
ViewModel:
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private bool isItemVisible;
public bool IsItemVisible { get { return isItemVisible; } set { isItemVisible = value; OnPropertyChanged("IsItemVisible"); } }
public ViewModel()
{
this.IsItemVisible = true;
}
private void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
I have the following WPF Combobox:
<Window.Resources>
<CollectionViewSource x:Key="performanceItemsource" Source="{Binding Path=SelectedReport.Performances}" >
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="Name"/>
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
</Window.Resources>
...
<ComboBox Name="cbxPlanPerf" Grid.ColumnSpan="2"
SelectedValuePath="MSDPortfolioID" DisplayMemberPath="Name"
SelectedValue="{Binding Path=PlanPerfID}"
ItemsSource="{Binding Source={StaticResource performanceItemsource}}"/>
The Source for the CollectionViewSource is:
public List<MSDExportProxy> Performances
{
get
{
if (Portfolio != null)
{
return (from a in Portfolio.Accounts where a.MSDPortfolioID != null select new MSDExportProxy(a))
.Concat<MSDExportProxy>(from g in Portfolio.Groups where g.MSDPortfolioID != null select new MSDExportProxy(g))
.Concat<MSDExportProxy>(from p in new[] { Portfolio } where p.MSDPortfolioID != null select new MSDExportProxy(p))
.ToList<MSDExportProxy>();
}
return new List<MSDExportProxy>();
}
}
The bound property PlanPerfID is a string.
I move between records using a ListBox control. The ComboBox works fine if the previous record had no items in its ComboBox.ItemsSource. If there were any items in the previous record's ComboBox.ItemsSource then the new record won't find its matching item in the ItemsSource collection. I've tried setting the ItemsSource in both XAML and the code-behind, but nothing changes this odd behavior. How can I get this darn thing to work?
Try using ICollectionViews in combination with IsSynchronizedWithCurrentItem property when handling lists / ObservableCollection in Xaml. The ICollectionView in the viewmodel can handle all the things needed, e.g. sorting, filtering, keeping track of selections and states.
Xaml:
<Window x:Class="ComboBoxBinding.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ListBox Grid.Column="0"
ItemsSource="{Binding Reports}"
DisplayMemberPath="Name"
IsSynchronizedWithCurrentItem="True" />
<ComboBox Grid.Column="1"
ItemsSource="{Binding CurrentReport.Performances}"
DisplayMemberPath="Name"
IsSynchronizedWithCurrentItem="True" />
</Grid>
</Window>
ViewModel:
public class ViewModel : INotifyPropertyChanged
{
private readonly IReportService _reportService;
private ObservableCollection<ReportViewModel> _reports = new ObservableCollection<ReportViewModel>();
private PerformanceViewModel _currentPerformance;
private ReportViewModel _currentReport;
public ObservableCollection<ReportViewModel> Reports
{
get { return _reports; }
set { _reports = value; OnPropertyChanged("Reports");}
}
public ReportViewModel CurrentReport
{
get { return _currentReport; }
set { _currentReport = value; OnPropertyChanged("CurrentReport");}
}
public PerformanceViewModel CurrentPerformance
{
get { return _currentPerformance; }
set { _currentPerformance = value; OnPropertyChanged("CurrentPerformance");}
}
public ICollectionView ReportsView { get; private set; }
public ICollectionView PerformancesView { get; private set; }
public ViewModel(IReportService reportService)
{
if (reportService == null) throw new ArgumentNullException("reportService");
_reportService = reportService;
var reports = _reportService.GetData();
Reports = new ObservableCollection<ReportViewModel>(reports);
ReportsView = CollectionViewSource.GetDefaultView(Reports);
ReportsView.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
ReportsView.CurrentChanged += OnReportsChanged;
ReportsView.MoveCurrentToFirst();
}
private void OnReportsChanged(object sender, EventArgs e)
{
var selectedReport = ReportsView.CurrentItem as ReportViewModel;
if (selectedReport == null) return;
CurrentReport = selectedReport;
if(PerformancesView != null)
{
PerformancesView.CurrentChanged -= OnPerformancesChanged;
}
PerformancesView = CollectionViewSource.GetDefaultView(CurrentReport.Performances);
PerformancesView.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
PerformancesView.CurrentChanged += OnPerformancesChanged;
PerformancesView.MoveCurrentToFirst();
}
private void OnPerformancesChanged(object sender, EventArgs e)
{
var selectedperformance = PerformancesView.CurrentItem as PerformanceViewModel;
if (selectedperformance == null) return;
CurrentPerformance = selectedperformance;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
I found a quick and dirty solution to my problem. I just happen to have a public NotifyPropertyChanged() method on my Report entity and I discovered that if I called SelectedReport.NotifyPropertyChanged("PlanPerfID") in the Report ListBox's SelectionChanged event that it was enough of a jolt to get the ComboBox to re-evaluate and find its matching item in the ItemsSource. Yeah, it's KLUGE...
UPDATE: I also wound up needing to add SelectedReport.NotifyPropertyChanged("Performances") for some situations...
UPDATE 2: Okay, turns out the above wasn't bullet proof and I ran across a situation that broke it so I had to come up with a better workaround:
Altered the SelectedReport property in the Window's code-behind, adding a private flag (_settingCombos) to keep the Binding from screwing up the bound values until the dust has settled from changin the ItemSource:
private bool _settingCombos = false;
private Report _SelectedReport;
public Report SelectedReport
{
get { return _SelectedReport; }
set
{
_settingCombos = true;
_SelectedReport = value;
NotifyPropertyChanged("SelectedReport");
}
}
Created a proxy to bind to in the Window code-behind that will refuse to update the property's value if the _settingCombos flag is true:
public string PlanPerfID_Proxy
{
get { return SelectedReport.PlanPerfID; }
set
{
if (!_settingCombos)
{
SelectedReport.PlanPerfID = value;
NotifyPropertyChanged("PlanPerfID_Proxy");
}
}
}
Added an extra Notification in the Report ListBox's SelectionChanged event along with code to reset the _settingCombos flag back to false:
private void lbxReports_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
//KLUGE: Couldn't get the ComboBoxes associated with these properties to work right
//this forces them to re-evaluate after the Report has loaded
if (SelectedReport != null)
{
NotifyPropertyChanged("PlanPerfID_Proxy");
_settingCombos = false;
}
}
Bound the ComboBox to the PlanPerfID_Proxy property (instead of directly to the SelectedReport.PlanPerfID property.
Wow, what a hassle! I think that this is simply a case of .NET's binding logic getting confused by the dynamic nature of the ComboBox.ItemSource, but this seems to have fixed it. Hope it helps someone else.
I have MainWindow containing a datagrid and a "filter panel". The filter panel can change by a user input(button click). I try to achieve it with databinding. The problem that Im facing is the filter panel(which is a user control) is not loaded or refreshed.
Mainwindow xaml:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250*" />
<ColumnDefinition Width="253*" />
</Grid.ColumnDefinitions>
<DataGrid AutoGenerateColumns="True" Height="200" HorizontalAlignment="Left" Margin="23,28,0,0" Name="dataGrid1" VerticalAlignment="Top" Width="200" ItemsSource="{Binding OverviewableItems}" />
<UserControl Grid.Column="1" Content="{Binding UserControl}" DataContext="{Binding}" Grid.ColumnSpan="2" />
<Button Content="PersonFilter" Height="23" HorizontalAlignment="Left" Margin="23,268,0,0" Name="buttonPersonFilter" VerticalAlignment="Top" Width="75" Click="buttonPersonFilter_Click" />
<Button Content="ProjectFilter" Height="23" HorizontalAlignment="Left" Margin="132,268,0,0" Name="buttonProjectFilter" VerticalAlignment="Top" Width="75" Click="buttonProjectFilter_Click" />
</Grid>
code behind:
private ViewModel _viewModel;
public MainWindow()
{
_viewModel = new ViewModel(new DataProvider());
DataContext = _viewModel;
_viewModel.PropertyChanged += _viewModel.SetFilterType;
InitializeComponent();
}
private void buttonProjectFilter_Click(object sender, RoutedEventArgs e)
{
_viewModel.OverviewType = OverviewType.Project;
}
private void buttonPersonFilter_Click(object sender, RoutedEventArgs e)
{
_viewModel.OverviewType = OverviewType.Person;
}
First user control:
<Grid>
<DatePicker Grid.Column="1" Grid.Row="1" Height="25" HorizontalAlignment="Left" Margin="19,18,0,0" Name="datePickerFundingTo" VerticalAlignment="Top" Width="115" Text="{Binding ElementName=ProjectFilter, Path=FundingTo}" />
</Grid>
code behind for this user control is only this:
public DateTime FundingTo { get; set; }
public ProjectFilter()
{
FundingTo = DateTime.Now;
InitializeComponent();
}
Other user control: just simply contains a TextBox and a Button, for the sake of simplicity I didnt add any code behind to it.
ViewModel of the MainWindow:
public class ViewModel : INotifyPropertyChanged
{
private UserControl _userControl;
public UserControl UserControl
{
get { return _userControl; }
set
{
if (_userControl == value)
{
return;
}
OnPropertyChanged("UserControl");
_userControl = value;
}
}
private OverviewType _overviewType = OverviewType.None;
public OverviewType OverviewType
{
get { return _overviewType; }
set
{
if (_overviewType == value)
{
return;
}
OnPropertyChanged("OverviewType");
_overviewType = value;
}
}
private ObservableCollection<IOverviewItem> _overviewableItems;
public ObservableCollection<IOverviewItem> OverviewableItems
{
get { return _overviewableItems; }
set
{
if (_overviewableItems == value)
{
return;
}
_overviewableItems = value;
}
}
private readonly DataProvider _dataProvider;
public event PropertyChangedEventHandler PropertyChanged;
public ViewModel(DataProvider dataProvider)
{
_dataProvider = dataProvider;
}
public void SetFilterType(object sender, EventArgs eventArgs)
{
switch (_overviewType)
{
case OverviewType.Project:
_userControl = new ProjectFilter();
break;
case OverviewType.Person:
_userControl = new PersonFilter();
break;
}
}
public void OnPropertyChanged(string name)
{
if (PropertyChanged == null)
return;
var eventArgs = new PropertyChangedEventArgs(name);
PropertyChanged(this, eventArgs);
}
}
plus I have an enum OverviewType with None,Project,Person values.
The property changed event fired properly, but the user control is not refreshed. Could anyone enlight me, where is the flaw in my solution?
And the other question I have, how can I communicate from the usercontrols to the mainwindow viewmodel? Forex: the datagrid should be changed according to its filter.
Any help would be greatly appreciated. Cheers!
There are different problems here.
As Clemens said, you must fire your event after the value is updated. But it's not the main issue here.
Second problem: you are affecting your new usercontrol to the private member, so you're totally bypassing your property.
Replace
_userControl = new ProjectFilter();
by
this.UserControl = new ProjectFilter();
Third problem, which is not directly related to your question but actually is your biggest problem: you have an architecture design issue. You're exposing in your viewmodel a UserControl, which is an anti-pattern. Your viewmodel must not know anything about the view, so it must NOT have any reference to the controls inside the view. Instead of the binding you wrote, you could fire an event from the viewmodel and add an event handler in your view so it's your view that updates the usercontrol.
Try to fire the PropertyChanged after changing a property's backing field:
public UserControl UserControl
{
get { return _userControl; }
set
{
if (_userControl != value)
{
_userControl = value; // first
OnPropertyChanged("UserControl"); // second
}
}
}
Similar for OverviewType.
I'm still somewhat new to WPF (only done a few small projects with it). I'm trying to make a repeating group of controls (user can add/remove these groups), databound to a custom class. Example UI:
([UserButton1] [UserButton2]) <--each of these () is a separate group of buttons
([Cheese] [Wine] )
([Wallace] [Gromit] )
[Add] <--this button can add more groups
databound to a list of a class like this (pseudocode):
class UserButtons {
string UserButton1 = "UserButton1"
string UserButton2 = "UserButton2"
}
such as
List<UserButtons> = {
[0]: UserButton1, UserButton2
[1]: Cheese, Wine
[2]: Wallace, Gromit
}
I know this is the sort of thing WPF was created to do, but I can't quite figure out exactly how to go about it.
Should I use some sort of ListView? Would a DataTemplate help? A StackPanel sounds OK, but it doesn't have databinding for a list...or does it? And I'm not even sure how to make databinding work for the groups of buttons like indicated above (if that even made sense to you...sorry for the bad example). Does anyone have any insight on this problem?
I searched to try to find a question pertaining to this and didn't see one, perhaps because I wasn't sure what to search for. So, sorry if it's an unintended dupe.
I'm not entirely sure what you're looking for but I hope the example below helps. I used an ItemsControl whose ItemsSource is set to the collection of UserButtons. Its ItemTemplate property is set to a StackPanel that shows two buttons, the Content property of each is bound to properties in UserButtons.
XAML:
<Window x:Class="WpfApplication3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication3"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
</Window.Resources>
<StackPanel Orientation="Vertical">
<ItemsControl x:Name="itemsControl" Background="LightBlue">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Button Content="{Binding Button1}" Width="100"/>
<Button Content="{Binding Button2}" Width="100"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Width="50" Click="Button_Click">Add</Button>
</StackPanel>
</Window>
Code-Behind:
public partial class MainWindow : Window
{
ObservableCollection<UserButtons> oc;
public MainWindow()
{
InitializeComponent();
oc = new ObservableCollection<UserButtons>()
{
new UserButtons() { Button1="UserButton1", Button2 = "UserButton2"},
new UserButtons() { Button1="Cheese", Button2 = "Wine"},
new UserButtons() { Button1="Wallace", Button2 = "Gromit"},
};
this.itemsControl.ItemsSource = oc;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
oc.Add(new UserButtons() { Button1 = "NewButton1", Button2 = "NewButton2" });
}
}
public class UserButtons : INotifyPropertyChanged
{
private string button1;
public string Button1
{
get { return this.button1; }
set
{
this.button1 = value;
this.OnPropertyChanged("Button1");
}
}
private string button2;
public string Button2
{
get { return this.button2; }
set
{
this.button2 = value;
this.OnPropertyChanged("Button2");
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
#endregion
}