I have my custom Calendar control - Event Calendar. I use it in a View of some case.
<Controls:EventCalendar Grid.Row="0"
Grid.RowSpan="8"
Grid.Column="2"
Margin="20,50,0,0"
CalendarEvents="{Binding DataContext.CalendarEvents, RelativeSource={RelativeSource TemplatedParent}, UpdateSourceTrigger=PropertyChanged}"
Header="{Binding DataContext.DataSpis.Header, RelativeSource={RelativeSource TemplatedParent}, UpdateSourceTrigger=PropertyChanged}"
ViewModelBase="{Binding DataContext.ViewModel, RelativeSource={RelativeSource TemplatedParent}, UpdateSourceTrigger=PropertyChanged}"
IsFunctionalityVisible="{Binding DataContext.IsFunctionalityVisible, RelativeSource={RelativeSource TemplatedParent}, UpdateSourceTrigger=PropertyChanged}"
IsCaseLoaded="{Binding DataContext.IsLoaded, RelativeSource={RelativeSource TemplatedParent}, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}">
</Controls:EventCalendar>
I detect if the case is loaded (same view, different data) via IsCaseLoaded Dependency Property. When this happens, I add new DataContext to my Calendar Control. Like this:
private static void LoadPCCallback(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
if (((EventCalendar)source).IsCaseLoaded == true)
{
((EventCalendar)source).DataContext = null;
((EventCalendar)source).DataContext = new EventCalendarViewModel(((EventCalendar)source).Header, ((EventCalendar)source).ViewModelBase, ((EventCalendar)source).CalendarEvents);
}
}
In constructor of EventCalendarViewModel I set some visibility for Meetings or Tasks I want to show. By default Meetings are shown and Tasks are hidden.
When I want to show Tasks, I click on Button on this Calendar Control.
And now where the behaviour starts to be unexpected: I load the Case, click on Tasks Button, it works - Tasks are shown, Meetings are hidden.
I reload the Case, click on Tasks Button, it works - Tasks are shown, Meetings are hidden.
But third time I reload the Case (sometimes second, sometimes fourth - really random), Constructor works, sets Meetings as default, but when I click on Tasks Button, it suddenly has values from previous DataContext, so it thinks Tasks are true, Meetings are false... so nothing changes and Meetings are still shown.
public void ShowMeetingsButtonClick()
{
this.ShowTasks = false;
NotifyOfPropertyChange(() => ShowTasks);
this.ShowMeetings = true;
NotifyOfPropertyChange(() => ShowMeetings);
}
Show Tasks is also like that:
public void ShowTasksButtonClick()
{
this.ShowMeetings = false;
NotifyOfPropertyChange(() => ShowMeetings);
this.ShowTasks = true;
NotifyOfPropertyChange(() => ShowTasks);
}
So one thing that comes to my mind is, somehow this View of Calendar founds previous DataContext in Visual Tree and takes old values from there. Because after constructor of new DataContext everything seems fine, but after clicking on a button it suddenly has different values.
I also thought some of my threads are changing something, but I tried to debug it and no one them (only Main Thread) are active during this.
Ok, I try to rebuild some stuff to simulate your behavior. And came up with this and it should be fairly close to the behavior your are heading towards.
I added an InverseToBooleanConverter
which show the the visibilty in the opposite way of the bool (false = Visible). This helps with the toggling stuff
I added a Converter for the GridLength (Your Height) coming from an Integer. And I took the liberty to create an Enum which represents the value of Show
and Hide
.
Important rule keep your ViewModels pure no Namespaces that starts with System.Windows or any view related stuff.
I somehow sorted your Properties and the PropertyNotification-Stuff. Goal is to keep it as tight and lean as possible. So for this code I only had calls to OnPropertyChanged from within the property itself.
For me the TaskListView
is a Control
and will have it's own ListOfTaskViewModel
(with behavior) and it's collection of Tasks (depending on the complexity in it. This could also be an ObservableList<TaskItemViewModel>
)
The same will apply for the MeetingListView
with it's MeetingListViewModel
.
Now it is important where and how to load Data. I could think about a Service which has at least 2 methods GetTasksForCaseID
and GetMeetingsForCaseID
which could be injected in the ViewModel or the loaded data could be passed on. I prefer to keep things independent and would use some thing like an EventAggregator
or a Messenger
to notify the ViewModel
with the matching ID as payload. And keep the responsibility to the ViewModel
to fetch the data. But this depends and since I had not enough information about your context this was out of the scope for the example. But I hope you get the idea.
This right here is the MainViewModel class
The same thing would also apply for your actual Events in the calendar and the highlight stuff. It Should be separated in a own ViewModel with own view control to keep things clean.
public class MainViewModel:INotifyPropertyChanged
{
public MainViewModel()
{
Init();
}
public enum Calendar{
ShowCalendarMaxLength = 145,
HideCalenderHeight = 325,
}
private MeetingsListViewModel _listOfMeetingsViewModel;
public MeetingsListViewModel ListOfMeetingsViewModel {
get { return _listOfMeetingsViewModel; }
set
{
if (_listOfMeetingsViewModel != value)
{
_listOfMeetingsViewModel = value;
OnPropertyChanged("ListOfMeetings");
}
}
}
public TaskListViewModel _listOfTasksViewModel;
public TaskListViewModel ListOfTasksViewModel {
get{return _listOfTasksViewModel;}
set {
if (_listOfTasksViewModel != value)
{
_listOfTasksViewModel = value;
OnPropertyChanged("ListOfTasks");
}
}
}
private Calendar _calendarEventListBoxHeight;
public Calendar CalendarEventListBoxHeight
{
get { return _calendarEventListBoxHeight; }
set
{
if (_calendarEventListBoxHeight != value)
{
_calendarEventListBoxHeight = value;
OnPropertyChanged("CalendarEventListBoxHeight");
}
}
}
private bool _showCalendar;
public bool ShowCalendar
{
get { return _showCalendar; }
set {
if (_showCalendar != value)
{
_showCalendar = value;
OnPropertyChanged("ShowCalendar");
}
}
}
private bool _showTasks;
public bool ShowTasks
{
get { return _showTasks; }
set
{
if (_showTasks != value)
{
_showTasks = value;
OnPropertyChanged("ShowTasks");
}
}
}
private bool _showMeetings;
public bool ShowMeetings
{
get { return _showMeetings; }
set
{
if (_showMeetings != value)
{
_showMeetings = value; OnPropertyChanged("ShowMeetings");
}
}
}
public void ShowCalendarAction()
{
ShowCalendar = true;
CalendarEventListBoxHeight = Calendar.ShowCalendarMaxLength;
}
public void HideCalendarAction()
{
ShowCalendar = false;
CalendarEventListBoxHeight = Calendar.HideCalenderHeight;
}
public void ShowMeetingsAction()
{
ShowTasks = false;
ShowMeetings = true;
}
public void ShowTasksAction() {
ShowMeetings = false;
ShowTasks = true;
}
private void Init()
{
ShowCalendar = true;
CalendarEventListBoxHeight = Calendar.ShowCalendarMaxLength;
ShowMeetings = true;
ShowTasks = false;
ListOfMeetingsViewModel = new MeetingsListViewModel();
ListOfTasksViewModel = new TaskListViewModel();
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
And this is the 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:conv="clr-namespace:WpfApplication1.Converters"
xmlns:vm="clr-namespace:WpfApplication1.ViewModels"
xmlns:cal="http://www.caliburnproject.org"
xmlns:views="clr-namespace:WpfApplication1.Views"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
Title="MainWindow" Height="350" Width="525"
>
<Window.Resources>
<BooleanToVisibilityConverter x:Key="VisibilityConverter"/>
<conv:InverseBooleanConverter x:Key="InverseVisibilityConverter"/>
<conv:GridViewLengthConverter x:Key="LengthConverter" />
</Window.Resources>
<Window.DataContext>
<vm:MainViewModel />
</Window.DataContext>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" Grid.Row="0">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<Calendar Grid.Row="0" Grid.Column="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="0,0,0,0"
Visibility="{Binding Path=ShowCalendar, Mode=TwoWay,Converter={StaticResource VisibilityConverter}}"
>
</Calendar>
<Button Margin="0,12,0,0"
FontSize="15"
Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Content="Show Calendar"
Visibility="{Binding Path=ShowCalendar,Mode=TwoWay,Converter={StaticResource InverseVisibilityConverter}}"
ToolTip="ShowCalendar">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<cal:ActionMessage MethodName="ShowCalendarAction" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
<Button Margin="0,32,0,0"
FontSize="15"
Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Visibility="{Binding Path=ShowCalendar,Mode=TwoWay,Converter={StaticResource VisibilityConverter}}"
Content="Hide Calendar"
ToolTip="HideCalendarButtonClick">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<cal:ActionMessage MethodName="HideCalendarAction" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
<Button Margin="0,5,0,0"
Grid.Row="1"
Grid.Column="0"
FontSize="15"
HorizontalAlignment="Left"
Visibility="{Binding Path=ShowMeetings,Mode=TwoWay,Converter={StaticResource InverseVisibilityConverter}}"
Content="Show Meetings"
ToolTip="ShowMeetingsButtonClick">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<cal:ActionMessage MethodName="ShowMeetingsAction" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
<Button Margin="20,5,0,0"
Grid.Row="1"
Grid.Column="0"
FontSize="15"
Grid.ColumnSpan="3"
HorizontalAlignment="Left"
Visibility="{Binding Path=ShowTasks,Mode=TwoWay,Converter={StaticResource InverseVisibilityConverter}}"
Content="Show Tasks;"
ToolTip="ShowTasksButtonClick">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<cal:ActionMessage MethodName="ShowTasksAction" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
<Grid Grid.Row="2"
Grid.RowSpan="3"
Grid.Column="0"
Grid.ColumnSpan="2"
MaxHeight="{Binding Path=CalendarEventListBoxHeight, Mode=TwoWay, Converter={StaticResource LengthConverter }}"
Visibility="{Binding Path=ShowMeetings, Mode=TwoWay,Converter={StaticResource VisibilityConverter}}"
>
<views:MeetingsListView DataContext="{Binding Path=ListOfMeetingsViewModel,Mode=TwoWay}">
</views:MeetingsListView>
</Grid>
<Grid Grid.Row="2"
Grid.RowSpan="3"
Grid.Column="0"
Grid.ColumnSpan="2"
MaxHeight="{Binding Path=CalendarEventListBoxHeight, Converter={StaticResource LengthConverter }}"
Visibility="{Binding Path=ShowTaks,Converter={StaticResource LengthConverter}}"
>
<views:TaskListView DataContext="{Binding Path=ListOfTasksViewModel,Mode=TwoWay}" />
</Grid>
</Grid>
</Grid>
</Window>
For the sake of completeness the two converters:
InverseBooleanToVisibiltyConverter
public class InverseBooleanConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (targetType != typeof(Visibility))
throw new InvalidOperationException("The target must be a boolean");
if (!(bool)value)
{
return Visibility.Visible;
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
#endregion
}
GridViewLengthConverter
class GridViewLengthConverter:IValueConverter{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
double val = (int)value;
GridLength gridLength = new GridLength(val);
return gridLength;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
GridLength val = (GridLength)value;
return val.Value;
}
}
I guess you could remove some code by optimizing your toggle behavior with less booleans =)...
//Edit: I strongly believe that the issue is outside the code you have shown. Especially the loading and exchange part or what was describe in your comment as "a lot more complex functionality" in the case ViewMode. Nevertheless since you have already an IsCaseLoaded-Property in place. I assume you are doing some async data fetching here. Async/await could also be tricky with MVVM. Especially when mixing UI-related operations with background operations. Attached you find some helpful links how to deal with async code and MVVM. This series shows approaches for async-bindable-notification-properties, async IComannd implementation and async services.
Async Programming : Patterns for Asynchronous MVVM Applications: Data Binding https://msdn.microsoft.com/en-us/magazine/dn605875.aspx
Async Programming : Patterns for Asynchronous MVVM Applications: Commands https://msdn.microsoft.com/en-us/magazine/dn630647.aspx
Async Programming : Patterns for Asynchronous MVVM Applications: Services https://msdn.microsoft.com/en-us/magazine/dn683795.aspx
Hope that helps...