ViewModel-Binding of parent view from within a (child) ItemsControl - c#

In our C# WPF application (with the Caliburn.Micro framework) we have a View and a ViewModel. In the ViewModel we have a string-property and I want to show this string inside each child of an ItemsControl (these items have their own ViewModel). I know I could just pass the property to each of these items, but that shouldn't be needed.
So, here is the relevant part of the ViewModel:
using System;
...
namespace NatWa.MidOffice.Modules.Financien.Views
{
public class BankgarantieFinancienOpsplitsenViewModel : ValidationBase<BankgarantieFinancienOpsplitsenViewModel>
{
...
public BankgarantieFinancienOpsplitsenViewModel(BankgarantieFinancienState state, ...)
{
...
Dossiernummer = state.Dossiernummer;
Kopers = state.Kopers.Select(k =>
{
var bfkvm = new BankgarantieFinancienKoperViewModel(k, adresService);
bfkvm.ObservePropertyChanged(koper => koper.Bedrag).Subscribe(p => CalculateOpenstaandBedrag());
return bfkvm;
}).ToList();
...
}
public string Dossiernummer
{
get { return _dossiernummer; }
private set
{
if (value == _dossiernummer) return;
_dossiernummer = value;
NotifyOfPropertyChange(() => Dossiernummer);
}
}
...
}
}
The relevant part of the View:
<Window x:Class="NatWa.MidOffice.Modules.Financien.Views.BankgarantieFinancienOpsplitsenView"
...>
<Grid Style="{StaticResource WindowPaddingStyle}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
...
</Grid.RowDefinitions>
...
<ItemsControl x:Name="Kopers" Grid.Row="1">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="{StaticResource BorderBrush}" BorderThickness="1" Margin="0,3" Padding="5">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
...
<StackPanel Grid.Column="1" Orientation="Vertical">
...
<StackPanel Orientation="Horizontal" VerticalAlignment="Top" Margin="0,3" HorizontalAlignment="Right">
...
<!-- THIS IS WHERE I WANT TO DISPLAY THE DOSSIERNUMMER-PROPERTY OF THE PARENT VIEWMODEL -->
<TextBlock Text="{Binding ??Parent??.Dossiernummer}"/>
...
</StackPanel>
...
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
...
</Grid>
</Window>
I did try to replace the TextBox with the following, based on this SO-answer, but to now result:
<TextBlock Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.Dossiernummer}"/>
I've also tried to add the DataContext binding to the Window (even though Caliburn.Micro should do this automatically):
<Window x:Class="NatWa.MidOffice.Modules.Financien.Views.BankgarantieFinancienOpsplitsenView"
...
DataContext="{Binding}">

Ok, problem found.. >.>
Me and a co-worker tried a couple of more things, like:
Adding this to the window:
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DataContext="{d:DesignData BankgarantieFinancienOpsplitsenViewModel}" mc:Ignorable="d"
Changing the TextBlock-binding to:
<TextBlock Text="{Binding ElementName=Kopers, Path=DataContext.Dossiernummer}"/>
and a couple of more things, all to no avail.. In a couple of other Views we've used the exact same things with success, so we had no idea what could be wrong. And then it struck us..
So, what was the problem? The string in the property was null for the ViewModel I was testing this for.... Great start of the day to make such a stupid mistake.. So, we've changed setting the value in the ViewModel to this:
Dossiernummer = state.Dossiernummer ?? state.UbizzDossiernummer;
(The UbizzDossiernummer is the number of the old system we are replacing (which we've imported into our appliction), and the Dossiernummer are the new numbers from Objects made in our application. The object I've been testing this for was an imported one..)
So, I've changed it back to:
<TextBlock Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.Dossiernummer}"/>
and it works..

Related

C# wpf caliburn.Micro MahApps HamburgerMenu.ContentTemplate data binding is not working

I'm making an application using Caliburn.Micro(for easy data binding and stuff) and MahApps.Metro(for designing).
I've created a View name 'MainView' which has HamburgerMenu of MahApps.
My issue is data binding is working fine under HamburgerMenu.ContentTemplate tag
Here is my HamburgerMenu.ContentTemplate xaml.
<Page x:Class="Sample.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cal="http://www.caliburnproject.org"
xmlns:mah="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
xmlns:utils="clr-namespace:Omni.WindowsClient.Utils"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Omni.WindowsClient.Views"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="600">
<Page.Resources>
<DataTemplate x:Key="HamburgerMenuItem"
DataType="{x:Type mah:HamburgerMenuItem}">
<Grid Height="48">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="48" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Image Margin="12"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Source="{Binding Glyph}"
Stretch="UniformToFill" />
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
FontSize="16"
Foreground="White"
Text="{Binding Label}" />
</Grid>
</DataTemplate>
</Page.Resources>
<Grid>
<mah:HamburgerMenu x:Name="HamburgerMenuControl"
SelectedIndex="0"
ItemTemplate="{StaticResource HamburgerMenuItem}"
OptionsItemTemplate="{StaticResource HamburgerMenuItem}"
IsPaneOpen="True"
DisplayMode="CompactInline"
cal:Message.Attach="[Event ItemClick] = [Action ShowDetails(HamburgerMenuControl.SelectedItem)]"
DataContext="{Binding RelativeSource={RelativeSource self}}">
<mah:HamburgerMenu.ItemsSource>
<mah:HamburgerMenuItemCollection>
<mah:HamburgerMenuItem Label="System Status">
<mah:HamburgerMenuItem.Tag>
<iconPacks:PackIconFontAwesome Width="22"
Height="22"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Kind="Tasks" />
</mah:HamburgerMenuItem.Tag>
</mah:HamburgerMenuItem>
<mah:HamburgerMenuItem Label="Inbox">
<mah:HamburgerMenuItem.Tag>
<iconPacks:PackIconFontAwesome Width="22"
Height="22"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Kind="Inbox" />
</mah:HamburgerMenuItem.Tag>
</mah:HamburgerMenuItem>
<mah:HamburgerMenuItem.Tag>
<iconPacks:PackIconFontAwesome Width="22"
Height="22"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Kind="Certificate" />
</mah:HamburgerMenuItem.Tag>
</mah:HamburgerMenuItem>
</mah:HamburgerMenuItemCollection>
</mah:HamburgerMenu.ItemsSource>
<mah:HamburgerMenu.ContentTemplate>
<DataTemplate DataType="{x:Type mah:HamburgerMenuItem}">
<Grid utils:GridUtils.RowDefinitions="48,*">
<!--cal:Action.TargetWithoutContext="{Binding ElementName=HamburgerMenuControl, Path=DataContext}"-->
<Border Grid.Row="0"
Background="{DynamicResource MahApps.Metro.HamburgerMenu.PaneBackgroundBrush}">
<TextBlock x:Name="Header"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="24"
Foreground="White" />
<!--Text="{Binding Path=Header, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"-->
</Border>
<Frame Grid.Row="1"
cal:Message.Attach="RegisterFrame($source)"
DataContext="{x:Null}"
NavigationUIVisibility="Hidden" />
</Grid>
</DataTemplate>
</mah:HamburgerMenu.ContentTemplate>
</mah:HamburgerMenu>
</Grid>
</Page>
and respective view model code is:
using Caliburn.Micro;
using MahApps.Metro.Controls;
using System.Windows.Controls;
namespace Sample.ViewModels
{
public class MainViewModel : Screen
{
private readonly SimpleContainer _container;
private INavigationService _navigationService;
private string _header;
public string HeaderTitle
{
get { return _header; }
set
{
_header = value;
NotifyOfPropertyChange();
}
}
public MainViewModel(SimpleContainer container)
{
this._container = container;
DisplayName = "Main";
}
public void RegisterFrame(Frame frame)
{
_navigationService = new FrameAdapter(frame);
_container.Instance(_navigationService);
_navigationService.NavigateToViewModel(typeof(SystemStatusViewModel));
HeaderTitle = "System Status";
}
public void ShowDetails(HamburgerMenuItem menuItem)
{
switch (menuItem.Label)
{
case "System Status":
_navigationService.NavigateToViewModel(typeof(SystemStatusViewModel));
HeaderTitle = "System Status";
break;
case "Inbox":
_navigationService.NavigateToViewModel(typeof(InboxViewModel));
HeaderTitle = "Inbox";
break;
default:
break;
}
}
}
}
I want to change View in frame under HamburgerMenu.ContentTemplate when I click on menu item.
Like System Status view is SystemStatusView
and Inbox view is InboxView.
My code is working fine (it changes the view in frame and change the Header label too) if I don't use HamburgerMenu.ContentTemplate. But I want to use HamburgerMenu.ContentTemplate to work with HamburgerMenu.
Thanks!
If it's working fine if you don't use HamburgerMenu.ContentTemplate, but stops working when you do, the problem is probably with you overwriting the default template in a way that doesn't support all functionalities of a control.
I'd suggest you to use Blend to get the default HamburgerMenu.ContentTemplate, then just edit it to your needs, without changing too much (keep in mind that names of controls used as a template may have a crucial meaning, so be careful what you are editing).
If you don't know how to use Blend to get your control's template, here is a simple tutorial described in a documentation of Telerik controls (don't worry, it works the same for all controls). You just need to create copy of a HamburgerMenu.ContentTemplate, paste it to your application and you are good to go (editing).

MVVM access Richtextbox flowdocument from VM

I am trying to make a RichTextBox with a FlowDocument that I can insert text at the caret position. I can add text to the end of the document. I think I am missing something in my setup to that allows my VM to access the Flowocument or I am setting it up wrong. If I create a FlowDocument in my VM and try to set my RichTextBox to it I get an error that my MyEditor (RichTextBox) does not exist. I can add text to the RichTextBox using what I call the AddItemBtn from a ListBox so at least that much works.
My question is "How should I set my RichTextBox/FlowDocument up?
XAML code
<Window x:Class="Scripter.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Scripter.ViewModels"
xmlns:wpftoolkit="clr-namespace:Xceed.Wpf.Toolkit;assembly=Xceed.Wpf.Toolkit"
Title="MainWindow" Height="350" Width="725">
<Grid HorizontalAlignment="Stretch">
<Grid HorizontalAlignment="Stretch" Height="72" Margin="10,14,0,0" VerticalAlignment="Top" Width="auto">
<WrapPanel HorizontalAlignment="Left" Height="50" Margin="10,0,0,0" VerticalAlignment="Top">
</WrapPanel>
<Button x:Name="OpenFilesBtn" Content="Open" HorizontalAlignment="Left" Margin="15,10,0,0" VerticalAlignment="Top" Width="75" Command="{Binding OpenFileBtn}"/>
<Button x:Name="SavefilesBtn" Content="Save" HorizontalAlignment="Left" Margin="104,10,0,0" VerticalAlignment="Top" Width="75" Command="{Binding SaveFileBtn}"/>
<TextBlock x:Name="OpenFile" Text="{Binding OpenFile,Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Margin="15,37,0,0" VerticalAlignment="Top" Width="353"/>
<ComboBox x:Name="TipsBtn" SelectedIndex="0" ItemsSource="{Binding Path=Tabs, UpdateSourceTrigger=PropertyChanged}" SelectedItem="{Binding Path=SelectedOption}" HorizontalAlignment="Left" Margin="538,10,0,0" VerticalAlignment="Top" Width="120"/>
<Button x:Name="AddItemBtn" Content="Add Item" HorizontalAlignment="Left" Margin="417,10,0,0" VerticalAlignment="Top" Width="100" Command="{Binding AddItemBtn}" CommandParameter="{Binding ElementName=AddItemList,Path=SelectedItem}"/>
</Grid>
<Grid Margin="10,100,10,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<RichTextBox Grid.Column="0" x:Name="MyEditor" SelectionChanged="MyEditor_SelectionChanged" ScrollViewer.VerticalScrollBarVisibility="Auto" Margin="0" Height="Auto" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Width="Auto" IsDocumentEnabled="True" AcceptsTab="True" AcceptsReturn="True" >
<RichTextBox.Resources>
<Style TargetType="{x:Type Paragraph}">
<Setter Property="Margin" Value="0" ></Setter>
<Setter Property="FontSize" Value="15"></Setter>
</Style>
</RichTextBox.Resources>
<FlowDocument >
<Paragraph >
<Run Text="{Binding TestText}" ></Run>
</Paragraph>
</FlowDocument>
</RichTextBox>
<ListBox x:Name="AddItemList" Grid.Column="1" Width="Auto" Height="Auto" ItemsSource="{Binding Path=OptionsToChoose}" SelectedItem="ItemSelected">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock x:Name="TextSelected" Text="{Binding Description}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Grid>
VM code that has the issue
public ScripterViewModel()
{
ScripterModel scripterModel = new ScripterModel();
ObservableCollection<string> tabsChoice = new ObservableCollection<string>();
tabsChoice.Add("Tabs");
tabsChoice.Add("Buttons");
Tabs = tabsChoice;
this.OpenFileBtn = new DelegateCommand(chooseFile, canChooseFile).ObservesProperty(() => OpenFile);
this.SaveFileBtn = new DelegateCommand(saveFile, canSaveFile).ObservesProperty(() => SaveFile);
this.AddItemBtn = new DelegateCommand<Tabbed>(addItem);
FlowDocument flowDoc = new FlowDocument();
Paragraph p = new Paragraph(new Run("new paragraph"));
flowDoc.Blocks.Add(new Paragraph(new Run("Paragraph 1")));
flowDoc.Blocks.Add(p);
//MyEditor = flowDoc;
}
public void MyEditor_SelectionChanged(object sender, RoutedEventArgs e)
{
// TextRange tempRange = new TextRange(MyEditor.Document.ContentStart, MyEditor.Selection.Start);
MessageBox.Show("Selection Changed");
}
private string _testText;
public string TestText
{
get
{
return _testText;
}
set
{
string _temp;
_temp = _testText + value;
SetProperty(ref _testText, value);
}
}
Hey I am new to WPF and MVVM but I'll give my best to help you. So don't blame me if I'm wrong.
1. Set Window.DataContext
First of all you have to tell your View where it can get the data from.
This can be done by adding this code to your View.xaml:
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
But make sure your namespace variable (here "local") points to your ViewModels.
xmlns:local="clr-namespace:Client.ViewModel"
This for ex. points to my ViewModel folder.
2. Define a OnPropertyChanged method
Your View won't know if you have modified a variable. So you need a method to notify your View about the changes.
First of all implement the interface INotifyPropertyChanged to your ViewModel.
public class MainViewModel : ViewModelBase, INotifyPropertyChanged
Now add this code:
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName]string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
3. Use OnPropertyChanged
So now you have a method to tell your View that a variable has changed but how do you use it?
To explain this to you I'll use your FlowDocument flowDoc.
So let's begin by defining your FlowDocument setting up :
private FlowDocument _flowDoc;
Now lets write a getter & setter for flowDoc:
public FlowDocument FlowDoc
{
get
{
return _flowDoc;
}
set
{
_flowDoc = value;
}
}
Now it's time to use our OnPropertyChanged method which we created in 2.
In the setter section you want to add the following Code:
OnPropertyChanged("variable");
Your result should now look like this:
public FlowDocument FlowDoc
{
get
{
return _flowDoc;
}
set
{
_flowDoc = value;
OnPropertyChanged("FlowDoc");
}
}
Important: remember to apply this to all your variables!
4. Use MVVM pattern right
In MVVM you have a Model a View and a ViewModel.
The Model is for your data so if possible don't store data in your ViewModel instead use a data class for ex.
You may have a look at this and/or this.
As I said in the beginning I'm new to all of this but i hope it helps
you. Feel free to ask.

Programmatically adjusting the number of rows in a grid with C#

I'm currently teaching myself XAML/C# and writing a calendar application. Right now it's creating a grid and then applying user control elements to the grid. It is correctly building my calendar but, instead of defining the number of rows in XAML, I want to be able to set the number via C# dynamically. Some months use more or less weeks (March needs 5 but April needs 6). I'm wondering how to do that or if I should be using a different control than grid.
This is what the UI looks like.
XAML code
<UserControl x:Class="CMS.Control.MonthView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch">
<Grid VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="40"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Background="AliceBlue">
<Image x:Name="MonthPrev" Source="/Images/Previous.png" Height="24" Margin="16,0,6,0" MouseLeftButtonUp="MonthPrev_MouseLeftButtonUp"/>
<Image x:Name="MonthNext" Source="/Images/Next.png" Height="24" Margin="6,0,16,0" MouseLeftButtonUp="MonthNext_MouseLeftButtonUp"/>
<Label x:Name="DateLabel" Content="January 2017" FontSize="16" FontFamily="Bold" VerticalAlignment="Center"/>
</StackPanel>
<Grid Grid.Row="1" Background="AliceBlue">
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="60" Width="*"/>
<ColumnDefinition MinWidth="60" Width="*"/>
<ColumnDefinition MinWidth="60" Width="*"/>
<ColumnDefinition MinWidth="60" Width="*"/>
<ColumnDefinition MinWidth="60" Width="*"/>
<ColumnDefinition MinWidth="60" Width="*"/>
<ColumnDefinition MinWidth="60" Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="Sunday" FontSize="9" Margin="2,0,0,2" Padding="0,1,0,0" HorizontalAlignment="Center" VerticalAlignment="Center" BorderThickness="0,0,1,0"/>
<Label Grid.Column="1" Content="Monday" FontSize="9" Margin="2,0,0,2" Padding="0,1,0,0" HorizontalAlignment="Center" VerticalAlignment="Center" BorderThickness="0,0,1,0"/>
<Label Grid.Column="2" Content="Tuesday" FontSize="9" Margin="2,0,0,2" Padding="0,1,0,0" HorizontalAlignment="Center" VerticalAlignment="Center" BorderThickness="0,0,1,0"/>
<Label Grid.Column="3" Content="Wednesday" FontSize="9" Margin="2,0,0,2" Padding="0,1,0,0" HorizontalAlignment="Center" VerticalAlignment="Center" BorderThickness="0,0,1,0"/>
<Label Grid.Column="4" Content="Thursday" FontSize="9" Margin="2,0,0,2" Padding="0,1,0,0" HorizontalAlignment="Center" VerticalAlignment="Center" BorderThickness="0,0,1,0"/>
<Label Grid.Column="5" Content="Friday" FontSize="9" Margin="2,0,0,2" Padding="0,1,0,0" HorizontalAlignment="Center" VerticalAlignment="Center" BorderThickness="0,0,1,0"/>
<Label Grid.Column="6" Content="Saturday" FontSize="9" Margin="2,0,0,2" Padding="0,1,0,0" HorizontalAlignment="Center" VerticalAlignment="Center" BorderThickness="0,0,1,0"/>
</Grid>
<Grid x:Name="WeekRowGrid" Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
</Grid>
</Grid>
</UserControl>
C# Code
namespace CMS.Control
{
/// <summary>
/// Interaction logic for MonthView.xaml
/// </summary>
public partial class MonthView : UserControl
{
private DateTime _DispayDate;
public MonthView()
{
InitializeComponent();
_DispayDate = DateTime.Now;
DrawMonth();
}
//Generates the
private void DrawMonth()
{
DateTime FirstDayOfMonth = new DateTime(_DispayDate.Year, _DispayDate.Month, 1);
int DisplayFrontPadding = (int)FirstDayOfMonth.DayOfWeek; // # of days that need to be displayed before the 1st of the month
int DaysInDisplayMonth = DateTime.DaysInMonth(_DispayDate.Year, _DispayDate.Month);
int DaysInDisplay = DisplayFrontPadding + DaysInDisplayMonth;
DaysInDisplay += 7 - DaysInDisplay%7; // Rounds up the displayed days to a multiple of 7
DateLabel.Content = _DispayDate.ToString("MMMM") + " " + _DispayDate.Year;
for (int i = 0; i<DaysInDisplay; i++)
{
DateTime DisplayDay = FirstDayOfMonth.AddDays(i - DisplayFrontPadding);
DayBox DB = DayBox.GetDay(); // DayBox factory
DB.DrawDay(DisplayDay);
Grid.SetRow(DB, i / 7);
WeekRowGrid.Children.Add(DB);
Grid.SetColumn(DB, i%7);
}
}
//Generates a calendar for the previous month on button click.
private void MonthPrev_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
_DispayDate = _DispayDate.AddMonths(-1);
DrawMonth();
}
//Generates a calendar for the next month on button click.
private void MonthNext_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
_DispayDate = _DispayDate.AddMonths(1);
DrawMonth();
}
}
}
I know I should really be using MVVM but I'm still wrapping my brain around programming in MVVM pattern and want to get this working. I'll probably refactor it once I'm more comfortable with it.
I just wanted to finish this project
Understood. The thing is, the basic idea behind MVVM isn't really all that hard, and if you embrace it, you likely will finish the project faster, than if you continue to try to hard-code all your UI. I can't guarantee that, of course. But I've been through the same thing, and I can tell you that you can spend a lot of time fighting WPF trying to configure the UI in code-behind explicitly.
Without a good Minimal, Complete, and Verifiable code example to start with, it wasn't practical for me to replicate exactly your user interface. But here is a simple code example that shows the basic approach you might take to use MVVM to build the UI you want…
First, it's helpful to have a base class that implements INotifyPropertyChanged for you. It simplifies the view model boilerplate a lot:
class NotifyPropertyChangedBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void _UpdateField<T>(ref T field, T newValue, [CallerMemberName] string propertyName = null)
{
if (!EqualityComparer<T>.Default.Equals(field, newValue))
{
field = newValue;
_OnPropertyChanged(propertyName);
}
}
protected virtual void _OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Then, we'll need view models. In this UI, there are two basic components: the overall view, and the individual days of the month. So, I made a view model for each:
class DateViewModel : NotifyPropertyChangedBase
{
private int _dayNumber;
private bool _isCurrent;
public int DayNumber
{
get { return _dayNumber; }
set { _UpdateField(ref _dayNumber, value); }
}
public bool IsCurrent
{
get { return _isCurrent; }
set { _UpdateField(ref _isCurrent, value); }
}
}
and…
class MonthViewViewModel : NotifyPropertyChangedBase
{
private readonly ObservableCollection<DateViewModel> _dates = new ObservableCollection<DateViewModel>();
private DateTime _selectedDate;
public DateTime SelectedDate
{
get { return _selectedDate; }
set { _UpdateField(ref _selectedDate, value); }
}
public IReadOnlyCollection<DateViewModel> Dates
{
get { return _dates; }
}
protected override void _OnPropertyChanged(string propertyName)
{
base._OnPropertyChanged(propertyName);
switch (propertyName)
{
case nameof(SelectedDate):
_UpdateDates();
break;
}
}
private void _UpdateDates()
{
_dates.Clear();
DateTime firstDayOfMonth = new DateTime(SelectedDate.Year, SelectedDate.Month, 1),
firstDayOfNextMonth = firstDayOfMonth.AddMonths(1);
int previousMonthDates = (int)firstDayOfMonth.DayOfWeek; // assumes Sunday-start week
int daysInView = previousMonthDates + DateTime.DaysInMonth(SelectedDate.Year, SelectedDate.Month);
// round up to nearest week multiple
daysInView = ((daysInView - 1) / 7 + 1) * 7;
DateTime previousMonth = firstDayOfMonth.AddDays(-previousMonthDates);
for (DateTime date = previousMonth; date < firstDayOfNextMonth; date = date.AddDays(1))
{
_dates.Add(new DateViewModel { DayNumber = date.Day, IsCurrent = date == SelectedDate.Date });
}
for (int i = 1; _dates.Count < daysInView; i++)
{
_dates.Add(new DateViewModel { DayNumber = i, IsCurrent = false });
}
}
}
As you can see, so far there's been no mention of UI, and yet already all the logic exists to build a month's worth of dates. The UI part, the XAML, will have no idea that you are doing anything related with months or dates. The closest it gets is a hard-coded invariant, i.e. the number of days in a week which are used to control the number of columns in the UniformGrid that will display your data.
The XAML looks like this:
<Window x:Class="TestSO43147585CalendarMonthView.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:p="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:l="clr-namespace:TestSO43147585CalendarMonthView"
xmlns:s="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d"
SizeToContent="Height"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<l:MonthViewViewModel SelectedDate="{x:Static s:DateTime.Today}"/>
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType="{x:Type l:MonthViewViewModel}">
<ItemsControl ItemsSource="{Binding Dates}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid IsItemsHost="True" Columns="7"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
<DataTemplate DataType="{x:Type l:DateViewModel}">
<Border BorderBrush="Black" BorderThickness="0, 0, 1, 0">
<StackPanel>
<TextBlock Text="{Binding DayNumber}">
<TextBlock.Style>
<p:Style TargetType="TextBlock">
<Setter Property="Background" Value="LightBlue"/>
<p:Style.Triggers>
<DataTrigger Binding="{Binding IsCurrent}" Value="True">
<Setter Property="Background" Value="Yellow"/>
</DataTrigger>
</p:Style.Triggers>
</p:Style>
</TextBlock.Style>
</TextBlock>
<Grid Height="{Binding ActualWidth, RelativeSource={x:Static RelativeSource.Self}}"/>
</StackPanel>
</Border>
</DataTemplate>
</Window.Resources>
<Grid>
<ContentControl Content="{Binding}" VerticalAlignment="Top"/>
</Grid>
</Window>
The XAML does three things:
It declares a MonthViewViewModel object to be used as the DataContext for the Window. An object in the visual tree, i.e. children of the Window, will inherit their parent's context if they have none of their own.
It declares data templates for the two view models. These tell WPF how it should visually represent the data. A view model contains the data that you want to represent, and this data is referenced in the template via {Binding...} syntax. In many cases (e.g. text, numbers, enum values), you can just bind directly and the default conversion will do what you want (as is the case above). If not, you can implement your own IValueConverter and incorporate that in the binding.
It provides a place for the MonthViewViewModel to be displayed, by declaring a ContentControl, where the content of that control is bound simply to the current data context (a {Binding} expression without a path will bind to the source, and the default source is the current data context).
In the context of the ContentControl, as well as the individual items being displayed in the ItemsControl, WPF will search for the template that is appropriate for the data object defined for that context, and will automatically populate your visual tree, binding to the necessary properties, according to that object.
There are a number of advantages to this approach, the primary ones being that you can describe your UI instead of having to code it, and that you maintain the OOP principle of "separation of concerns", which is key in reducing the mental work-load involved by allowing you to focus on one thing at a time, instead of having to deal with UI and data logic together.
A couple of side-notes regarding the XAML above:
You might notice that I have added the p: XML namespace and used it for the Style element. This is only work around a Stack Overflow bug, in which the Style element by itself confuses the XML formatter and prevents the element and its children from being formatted correctly. The XAML will compile fine like this, but it's not actually necessary in real code. In your regular XAML, you can safely omit it.
I included a feature your code didn't, just for the purpose of illustration. That is, the current date is highlighted in yellow. The technique shown here is very useful, as it allows you to customize the appearance of an item in a single template, based on property values of the view model. But there is a little trap: in WPF, if you explicitly set an element's property via the attribute syntax, e.g. something like <TextBlock Text="{Binding DayNumber}" Background="LightBlue">, then that syntax will take precedence over any <Setter...> elements in a style. You have to remember to set the default value of any property that you intend to set via a trigger, in its own <Setter...> in the style as well (as shown above).
You could create a method that adjust the number of rows automatically based on the number of weeks (rows). This method always remove all the rows and then add the correct number of rows you need.
private void AdjustRowDefinitions(int numberOfWeeks)
{
WeekRowGrid.RowDefinitions.Clear();
for (int i = 0; i < numberOfWeeks; i++)
{
RowDefinition rowDef = new RowDefinition();
rowDef.Height = new GridLength(1, GridUnitType.Star); //this sets the height of the row to *
WeekRowGrid.RowDefinitions.Add(rowDef);
}
}

How to display HeaderedItemsControl's Header?

I have the following code:
<Window.Resources>
<DataTemplate x:Key="SectionTemplate" >
<TextBlock Text="{Binding Path=Name}" />
</DataTemplate>
</Window.Resources>
<Grid >
<Border>
<HeaderedItemsControl Header="Top1"
ItemsSource="{Binding Path=List1}"
ItemTemplate="{StaticResource SectionTemplate}"/>
</Border>
</Grid>
public class MainWindow
{
public List<Item> List1
{
get { return list1; }
set { list1 = value; }
}
public MainWindow()
{
list1.Add(new Item { Name = "abc" });
list1.Add(new Item { Name = "xxx" });
this.DataContext = this;
InitializeComponent();
}
}
public class Item
{
public string Name { get; set; }
}
For some reason the Items are rendered, but without the header.
As the documentation points out:
A HeaderedItemsControl has a limited default style. To create a HeaderedItemsControl with a custom appearance, create a new ControlTemplate.
So when you create that template make sure to include some ContentPresenter which is bound to the Header (e.g. using ContentSource)
e.g.
<HeaderedItemsControl.Template>
<ControlTemplate TargetType="{x:Type HeaderedItemsControl}">
<Border>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<ContentPresenter ContentSource="Header" />
<Separator Grid.Row="1" />
<ItemsPresenter Grid.Row="2" />
</Grid>
</Border>
</ControlTemplate>
</HeaderedItemsControl.Template>
(All the default bindings (Margin, Background, etc.) are omitted.)
You can make a DataTemplate for the header, just as you did for the items (which is surely less intrusive than redoing the entire control template).
e.g.
<Window.Resources>
<DataTemplate x:Key="HeaderTemplate">
</DataTemplate>
</Window.Resources>
<HeaderedItemsControl Header="Top1"
HeaderTemplate="{StaticResource HeaderTemplate}"
ItemsSource="{Binding Path=List1}"
ItemTemplate="{StaticResource SectionTemplate}"
/>
Instead of using e.g. "Top1" as here, you can bind to an object and then use binding relative to that object in the DataTemplate.
There is one gotcha with this approach, which is that the necessary approach to getting styles pulled in for non-control elements (notably TextBlock) is a little convoluted; also see WPF Some styles not applied on DataTemplate controls. (You might also run into this with the ControlTemplate approach.)

How to access base datacontext in Collection in xaml

I have a wpf app with the datacontext set to an instance of a viewmodel class. It all works fine except where I need to access a property of the viewmodel in a listbox with the datacontext set to a collection that is contained in the ViewModel class. On msdn it says you can escape using the \ character but that has not worked for me
My code
public class StatusBoardViewModel : INotifyPropertyChanged
{
OIConsoleDataContext db = new OIConsoleDataContext();
// the collection
private IQueryable<Issue> issues;
public IQueryable<Issue> Issues
{
get
{
// Lazy load issues if they have not been instantiated yet
if (issues == null)
QueryIssues(); // This just runs a linq query to set the property
return issues;
}
set
{
if (issues != value)
{
issues = value;
OnPropertyChanged("Issues");
}
}
}
// The property I need to access
private bool showDetailListItems = true;
public bool ShowDetailListItems
{
get
{
return showDetailListItems;
}
set
{
if (showDetailListItems != value)
{
showDetailListItems = value;
OnPropertyChanged("ShowDetailListItems");
}
}
}
}
in the window1.xaml.cs
//instantiate the view model
StatusBoardViewModel statusBoardViewModel = new StatusBoardViewModel();
public Window1()
{
InitializeComponent();
// setting the datacontext
this.DataContext = statusBoardViewModel;
}
And the Xaml
// This is in the Window1.xaml file
<ListBox x:Name="IssueListBox"
ItemsSource="{Binding Issues}" // Binds the listbox to the collection in the ViewModel
ItemTemplate="{StaticResource ShowIssueDetail}"
IsSynchronizedWithCurrentItem="True"
HorizontalContentAlignment="Stretch" BorderThickness="3"
DockPanel.Dock="Top" VerticalContentAlignment="Stretch"
Margin="2" MinHeight="50" />
// The datatemplate from the app.xaml file
<DataTemplate x:Key="ShowIssueDetail">
<Border CornerRadius="3" Margin="2" MinWidth="400" BorderThickness="2"
BorderBrush="{Binding Path=IssUrgency, Converter={StaticResource IntToRYGBBoarderBrushConverter}}">
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Path=IssSubject}" Margin="3" FontWeight="Bold" FontSize="14"/>
<!--DataTrigger will collapse following panel for simple view-->
<StackPanel Name="IssueDetailPanel" Visibility="Visible" Margin="3">
<StackPanel Width="Auto" Orientation="Horizontal">
<TextBlock Text="Due: " FontWeight="Bold"/>
<TextBlock Text="{Binding Path=IssDueDate}" FontStyle="Italic" HorizontalAlignment="Left"/>
</StackPanel>
<StackPanel Width="Auto" Orientation="Horizontal">
<TextBlock Text="Category: " FontWeight="Bold"/>
<TextBlock Text="{Binding Path=IssCategory}"/>
</StackPanel>
</StackPanel>
</StackPanel>
</Border>
// This is where I have the issue, ShowDetailListItems is in the base class, not the collection
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=ShowDetailListItems, Mode=TwoWay}" Value="False">
<Setter TargetName="IssueDetailPanel" Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
I have learned a TON doing this but this current issue is driving me batty, no luck with google, MSDN, SO or a couple books
I thought I would add this note: I am learning this in order to build some apps for my business, I am a rank beginner in wpf and xaml so I relize this is probably somthing silly. I would really like to find a good tutorial on datacontexts as what I do find is a dozen different "How To's" that are all totally different. I know I have some big holes in my knowledge because I keep ending up with multiple instantiations of my viewmodel class when I try to create references to my datacontext in the codebehind, Window1.xaml and app.xaml files.
Have you tried one of these?
{Binding Path=ShowDetailListItems, ElementName=YourWindowName}
or
{Binding ShowDetailListItems, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}

Categories