UWP NavigationView switch to another page via MVVM - c#

Switching for the first time ever from WPF to UWP I found out that there are great controls like SplitView and NavgiationView do exist in the world of UWP.
For renewing my current home-project I have choosen the NavigationView control as my main UI control for offering the various pieces of information.
Using the code-behind the page navigation (as shown here) is very easy, but for my use-case I want to work with MVVM (as a learning procedure without using FWs like MVVMLIght or similar).
Currently, my NavigationView looks like this; but I have not a proper idea how to change within the frame through pages (from my understanding I have to use the NavigationService but haven't found a easy to understand example of this):
<NavigationView x:Name="nvTopLevelNav" Grid.Column="0" Grid.Row="1" Grid.RowSpan="3" IsPaneOpen="False" IsPaneToggleButtonVisible="False" CompactModeThresholdWidth="0" IsBackButtonVisible="Collapsed" Background="Black" Foreground="Black"
Loaded="nvTopLevelNav_Loaded"
Margin="0,12,0,0"
SelectionChanged="nvTopLevelNav_SelectionChanged"
ItemInvoked="nvTopLevelNav_ItemInvoked"
IsTabStop="False"
IsSettingsVisible="False"
AlwaysShowHeader="False"
Header="asdasdaasdasdasd">
<NavigationView.MenuItems>
<NavigationViewItem Icon="Home" Content="Home" Tag="Home_Page" />
<NavigationViewItem Icon="Globe" Content="Weather" Tag="Weather_Page" />
<NavigationViewItem Content="My Agenda" Tag="Agenda_Page">
<!-- some custom PathIcon -->
</NavigationViewItem>
<NavigationViewItem Icon="Contact" Content="My News" Tag="News_Page" />
</NavigationView.MenuItems>
<Frame x:Name="contentFrame"></Frame>
</NavigationView>

UWP NavigationView switch to another page via MVVM
For your requirement, you could use Windows Template Studio to create UWP project that contain MVVM pattern and NavigationService.
private void OnItemInvoked(NavigationViewItemInvokedEventArgs args)
{
if (args.IsSettingsInvoked)
{
NavigationService.Navigate(typeof(SettingsViewModel).FullName);
return;
}
var item = _navigationView.MenuItems
.OfType<NavigationViewItem>()
.First(menuItem => (string)menuItem.Content == (string)args.InvokedItem);
var pageKey = item.GetValue(NavHelper.NavigateToProperty) as string;
NavigationService.Navigate(pageKey);
}

Have a read of my blog about solution i came up with without any 3rd party api, there is also github repot with the solution.
https://kwodarczyk.github.io/UwpWinUI3PageMVVMNavigation/
All you need it attached property that allows to pass view model type to your datatempalte
public class ViewModelContext : DependencyObject
{
public static readonly DependencyProperty TypeProperty =
DependencyProperty.RegisterAttached(
"ViewModelContext",
typeof(Type),
typeof(ViewModelContext),
new PropertyMetadata(null)
);
public static void SetType(DependencyObject element, Type value)
{
element.SetValue(TypeProperty, value);
}
public static Type GetType(DependencyObject element)
{
return (Type)element.GetValue(TypeProperty);
}
}
example datatempalte:
<DataTemplate x:Key="PageOne" viewModels:ViewModelContext.Type="viewModels:PageOneViewModel">
<views:PageOne/>
</DataTemplate>
and custom template selector where you can match the template agatis the view model
public class CustomTemplateSelector : DataTemplateSelector
{
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
if (item != null)
{
var viewModeltype = item.GetType();
foreach (var rd in Application.Current.Resources.MergedDictionaries)
{
foreach (var value in rd.Values)
{
if (value is DataTemplate dataTempate)
{
Type contexType = dataTempate.GetValue(ViewModelContext.TypeProperty) as Type;
if (contexType != null && contexType == viewModeltype)
{
return dataTempate;
}
}
}
}
}
return null;
}
}

Related

How to use MVVM ViewModel to change stuff in a View based on a ListView SelectionChanged event

I want to make a UWP application with a ListBox navigation whose selected item determines the content of a frame. When the selection changes, the frame content should change. I found an example for how to this (Method 1 below), but it uses event handlers in the code behind. I want to learn MVVM with this project and thus want to use a MVVM solution to this problem. I'm new to MVVM and my current understanding is that in order to decouple the View from the ViewModel, ViewModels shouldn't reference anything particular to the View. Is this a correct understanding? The only way I can think of using the ViewModel to change the frame view is to basically move the event handler code to the ViewModel and pass in the Frame to the constructor (Method 2 below). But that violates my understanding of the ViewModel relationship to the View since the ViewModel is referencing particular instances of things in the View; furthermore it seems like pointless overhead and like it would give very little organization benefit.
How would you implement a MVVM solution to this problem? Or is this a case where using event handlers is better?
METHOD 1 - Event handler:
This code is based on an example Microsoft provides. (Here's the link to the relevant code: https://github.com/microsoft/Windows-universal-samples/tree/master/Samples/Playlists/cs)
public sealed partial class MainPage : Page
{
List<Scenario> scenarios = new List<Scenario>
{
new Scenario() { Title = "Scenario 1", ClassType = typeof(Scenario1) },
new Scenario() { Title = "Scenario 2", ClassType = typeof(Scenario2) }
};
public MainPage()
{
this.InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
ScenarioControl.ItemsSource = scenarios;
}
private void ScenarioControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ListBox scenarioListBox = sender as ListBox;
Scenario s = scenarioListBox.SelectedItem as Scenario;
if (s != null)
{
ScenarioFrame.Navigate(s.ClassType);
}
}
}
public class Scenario
{
public string Title { get; set; }
public Type ClassType { get; set; }
public override string ToString()
{
return Title;
}
}
<!-- MainPage.xaml -->
<Grid>
<SplitView x:Name="Splitter" IsPaneOpen="True" DisplayMode="Inline">
<SplitView.Pane>
<RelativePanel>
<ListBox x:Name="ScenarioControl" SelectionChanged="ScenarioControl_SelectionChanged"/>
</RelativePanel>
</SplitView.Pane>
<RelativePanel>
<Frame x:Name="ScenarioFrame" />
</RelativePanel>
</SplitView>
</Grid>
METHOD 2 - MVVM(?):
<!-- MainPage.xaml -->
<Grid>
...
<ListBox x:Name="ScenarioControl" SelectionChanged="{x:Bind MyViewModel.SwitchScenario}"/>
...
</Grid>
// MainPage.xaml.cs
...
public MainPage()
{
this.InitializeComponent();
MyViewModel = new MyViewModel(ScenarioFrame);
}
...
MyViewModel MyViewModel { get; set; }
}
// MyViewModel.cs
public class MyViewModel
{
public MyViewModel(Frame scenarioFrame)
{
ScenarioFrame = scenarioFrame;
}
public void SwitchScenario(object sender, SelectionChangedEventArgs e)
{
ListBox scenarioListBox = sender as ListBox;
Scenario s = scenarioListBox.SelectedItem as Scenario;
if (s != null)
{
ScenarioFrame.Navigate(s.ClassType);
}
}
public Frame ScenarioFrame { get; set; }
}
You’ll need PropertyChangedNotification when your model property changes- anything binding to the model property will then update automatically.
Also Bindings.Update() is your friend sometimes.
And if you’re going to have a View and a ViewModel you’ll need to set the DataContext of your View to the instance of the ViewModel or Model you are binding to.
How would you implement a MVVM solution to this problem? Or is this a case where using event handlers is better?
For implementing MVVM navigation, you could refer Template 10 and Template Studio workflow.
In template 10, it bind Click event with navigation method.
<controls:PageHeader x:Name="pageHeader" RelativePanel.AlignLeftWithPanel="True"
RelativePanel.AlignRightWithPanel="True"
RelativePanel.AlignTopWithPanel="True" Text="Main Page">
<!-- secondary commands -->
<controls:PageHeader.SecondaryCommands>
<AppBarButton Click="{x:Bind ViewModel.GotoSettings}" Label="Settings" />
<AppBarButton Click="{x:Bind ViewModel.GotoPrivacy}" Label="Privacy" />
<AppBarButton Click="{x:Bind ViewModel.GotoAbout}" Label="About" />
</controls:PageHeader.SecondaryCommands>
</controls:PageHeader>
ViewModel
public void GotoDetailsPage() =>
NavigationService.Navigate(typeof(Views.DetailPage), Value);
public void GotoSettings() =>
NavigationService.Navigate(typeof(Views.SettingsPage), 0);
In Template Studio, it navigates with NavHelper class.
<winui:NavigationViewItem x:Uid="Shell_Main" Icon="Document" helpers:NavHelper.NavigateTo="views:MainPage" />
<winui:NavigationViewItem x:Uid="Shell_Blank" Icon="Document" helpers:NavHelper.NavigateTo="views:BlankPage" />
<winui:NavigationViewItem x:Uid="Shell_MediaPlayer" Icon="Document" helpers:NavHelper.NavigateTo="views:MediaPlayerPage" />
<winui:NavigationViewItem x:Uid="Shell_WebView" Icon="Document" helpers:NavHelper.NavigateTo="views:WebViewPage" />
<ic:EventTriggerBehavior EventName="ItemInvoked">
<ic:InvokeCommandAction Command="{x:Bind ViewModel.ItemInvokedCommand}" />
</ic:EventTriggerBehavior>
ViewModel
private void OnItemInvoked(WinUI.NavigationViewItemInvokedEventArgs args)
{
if (args.IsSettingsInvoked)
{
NavigationService.Navigate(typeof(SettingsPage));
return;
}
var item = _navigationView.MenuItems
.OfType<WinUI.NavigationViewItem>()
.First(menuItem => (string)menuItem.Content == (string)args.InvokedItem);
var pageType = item.GetValue(NavHelper.NavigateToProperty) as Type;
NavigationService.Navigate(pageType);
}

Two Views in one ViewModel (WPF/MVVM)

PROBLEM: Use one single viewModel with two different views.
I have a Window with a control ContentControl which is binded to a property in the DataContext, called Object MainContent {get;set;}. Base on a navigationType enum property, I assign other ViewModels to it to show the correct UserControl.
I need to merge two views into one ViewModel, and because I'm assigning a ViewModel to the ContentControl mentioned before, the TemplateSelector is not able to identify which is the correct view as both shares the same viewModel
If I assign the view instead the ViewModel to the ContentControl, the correct view is shown, however, non of the commands works.
Any Help? Thanks in advance.
SOLUTION: based on #mm8 answer and https://stackoverflow.com/a/5310213/2315752:
ManagePatientViewModel.cs
public class ManagePatientViewModel : ViewModelBase
{
public ManagePatientViewModel (MainWindowViewModel inMainVM) : base(inMainVM) {}
}
ViewHelper.cs
public enum ViewState
{
SEARCH,
CREATE,
}
MainWindowViewModel.cs
public ViewState State {get;set;}
public ManagePatientViewModel VM {get;set;}
private void ChangeView(ViewState inState)
{
State = inState;
// This is need to force the update of Content.
var copy = VM;
MainContent = null;
MainContent = copy;
}
public void NavigateTo (NavigationType inNavigation)
{
switch (inNavigationType)
{
case NavigationType.CREATE_PATIENT:
ChangeView(ViewState.CREATE);
break;
case NavigationType.SEARCH_PATIENT:
ChangeView(ViewState.SEARCH);
break;
default:
throw new ArgumentOutOfRangeException(nameof(inNavigationType), inNavigationType, null);
}
}
MainWindow.xaml
<DataTemplate x:Key="CreateTemplate">
<views:CreateView />
</DataTemplate>
<DataTemplate x:Key="SearchTemplate">
<views:SearchView/>
</DataTemplate>
<TemplateSelector x:Key="ViewSelector"
SearchViewTemplate="{StaticResource SearchTemplate}"
CreateViewTemplate="{StaticResource CreateTemplate}"/>
<ContentControl
Grid.Row="1"
Content="{Binding MainContent}"
ContentTemplateSelector="{StaticResource ViewSelector}" />
TemplateSelector.cs
public class TemplateSelector : DataTemplateSelector
{
public DataTemplate SearchViewTemplate {get;set;}
public DataTemplate CreateViewTemplate {get;set;}
}
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (!(item is SelectLesionViewModel vm))
{
return null;
}
switch (vm.ViewType)
{
case ViewState.CREATE:
return CreateViewTemplate;
case ViewState.SEARCH:
return SearchViewTemplate;
default:
return null;
}
}
}
How is the TemplateSelector supposed to know which template to use when there are two view types mapped to a single view model type? This makes no sense I am afraid.
You should use two different types. You could implement the logic in a common base class and then define two marker types that simply derive from this implementation and add no functionality:
public class ManagePatientViewModel { */put all your code in this one*/ }
//marker types:
public class SearchPatientViewModel { }
public class CreatePatientViewModel { }
Also, you don't really need a template selector if you remove the x:Key attributes from the templates:
<DataTemplate DataType="{x:Type viewModels:SearchPatientViewModel}">
<views:SearchPatientView />
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:CreatePatientViewModel}">
<views:CreatePatientView />
</DataTemplate>
...
<ContentControl
Grid.Row="1"
Content="{Binding MainContent}" />
Maybe the requirement is to switch out the views and retain the one viewmodel.
Datatemplating is just one way to instantiate a view.
You could instead set the datacontext of the contentcontrol to the instance of your viewmodel and switch out views as the content. Since views are rather a view responsibility such tasks could be done completely in the view without "breaking" mvvm.
Here's a very quick and dirty approach illustrating what I mean.
I build two usercontrols, UC1 and UC2. These correspond to your various patient views.
Here's the markup for one:
<StackPanel>
<TextBlock Text="User Control ONE"/>
<TextBlock Text="{Binding HelloString}"/>
</StackPanel>
I create a trivial viewmodel.
public class OneViewModel
{
public string HelloString { get; set; } = "Hello from OneViewModel";
}
My mainwindow markup:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel>
<Button Content="UC1" Click="UC1_Click"/>
<Button Content="UC2" Click="UC2_Click"/>
</StackPanel>
<ContentControl Name="parent"
Grid.Column="1"
>
<ContentControl.DataContext>
<local:OneViewModel/>
</ContentControl.DataContext>
</ContentControl>
</Grid>
The click events switch out the content:
private void UC1_Click(object sender, RoutedEventArgs e)
{
parent.Content = new UC1();
}
private void UC2_Click(object sender, RoutedEventArgs e)
{
parent.Content = new UC2();
}
The single instance of oneviewmodel is retained and the view shown switches out. The hellostring binds and shows ok in both.
In your app you will want a more sophisticated approach to setting that datacontext but this sample is intended purely as a proof of concept to show you another approach.
Here's the working sample:
https://1drv.ms/u/s!AmPvL3r385QhgpgMZ4KgfMWUnxkRzA

RelayCommand not getting the right Model

I created a user control that looks like a tile. Created another user control named TilePanel that serves as the default container of the tiles. And lastly, the very UI that looks like a Window start screen. I used RelayCommand to bind my TileCommands
Here are the codes:
Tilev2.xaml
<UserControl x:Class="MyNamespace.Tilev2"
Name="Tile"....
>
...
<Button x:Name="btnTile" Style="{StaticResource TileStyleButton}" Command="{Binding ElementName=Tile, Path=TileClickCommand}" >
</Button>
</UserControl>
Tilev2.xaml.cs
public partial class Tilev2 : UserControl
{
public Tilev2()
{
InitializeComponent();
}
//other DPs here
public ICommand TileClickCommand
{
get { return (ICommand)GetValue(TileClickCommandProperty); }
set { SetValue(TileClickCommandProperty, value); }
}
// Using a DependencyProperty as the backing store for TileClickCommand. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TileClickCommandProperty =
DependencyProperty.Register("TileClickCommand", typeof(ICommand), typeof(Tilev2));
}
}
Then I created a TilePanel user control as the container of the tiles
TilePanel.xaml
<UserControl x:Class="MyNamespace.TilePanel"
...
>
<Grid>
<ScrollViewer>
<ItemsControl Name="tileGroup"
ItemsSource="{Binding TileModels}" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local2:Tilev2 TileText="{Binding Text}"
TileIcon="{Binding Icon}"
TileSize="{Binding Size}"
TileFontSize="{Binding FontSize}"
Background="{Binding Background}"
TileCaption="{Binding TileCaption}"
TileCaptionFontSize="{Binding TileCaptionFontSize}"
TileClickCommand="{Binding TileCommand}"
/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</UserControl>
TilePanel.xaml.cs
public partial class TilePanel : UserControl
{
public TilePanel()
{
InitializeComponent();
DataContext = new TilePanelViewModel();
}
public TilePanelViewModel ViewModel
{
get { return (TilePanelViewModel)this.DataContext; }
}
}
My ViewModel for TilePanel
TilePanelViewModel.cs
public class TilePanelViewModel : ViewModelBase
{
private ObservableCollection _tileModels;
public ObservableCollection<TileModel> TileModels
{
get
{
if (_tileModels == null)
_tileModels = new ObservableCollection<TileModel>();
return _tileModels;
}
}
}
Then my Tile model
TileModel.cs
public class TileModel : BaseNotifyPropertyChanged
{
//other members here
ICommand tileCommand { get; set; }
//other properties here
public ICommand TileCommand
{
get { return tileCommand; }
set { tileCommand = value; NotifyPropertyChanged("TileCommand"); }
}
}
}
This is my StartScreen View where TilePanels with tiles should be displayed...
StartScreen.xaml
<UserControl x:Class="MyNamespace.StartMenu"
... >
<Grid>
<DockPanel x:Name="dockPanel1" Grid.Column="0" Grid.Row="1" Margin="50,5,2,5">
<local:TilePanel x:Name="tilePanel"></local:TilePanel>
</DockPanel>
</Grid>
</UserControl>
StartScreen.xaml.cs
public partial class WincollectStartMenu : UserControl, IView<StartMenuViewModel>
{
public WincollectStartMenu()
{
InitializeComponent();
}
public StartMenuViewModel ViewModel { get { return (DataContext as StartMenuViewModel); } }
private void UserControl_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
ViewModel.Tile = tilePanel.ViewModel.TileModels;
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
return;
}
}
In my start screen ViewModel, I used ObservableCollection Tile
and use Tile.Add(tile); to populate my start screen with Tiles inside the TilePanel...
StartMenuViewModel.cs
TileModel tile = new TileModel() { Text = "Testing1", FontSize = 11, Size = TileSize.Medium, Background = (SolidColorBrush)new BrushConverter().ConvertFromString("#039BE5"), Tag="Something" };
tile.TileCommand = new RelayCommand(
p => Tile_TileClick(tile.Tag),
p => true
);
temp.Add(tile);
Now the problem is, if I add a new code below, tile = new TileModel() {...}
tile.TileCommand = new RelayCommand(...), even if I clicked on the first tile, my Tile_TileClick() will get the second tile's info (or the last tile inserted)...
Am I doing something wrong? Or Im doing everything wrong...?
This is not direct answer to your question, but hopefully it will give you few thoughts.
Ok, first of all, don't name your usercontrol like this:
<UserControl x:Class="MyNamespace.Tilev2" Name="Tile"/>
because the name can be easily overriden when using the usercontrol somewhere:
<local:Titlev2 Name="SomeOtherName" />
and the binding inside Tilevs with ElementName won't work: Command="{Binding ElementName=Tile, Path=TileClickCommand}"
Second, what's the point of Tilev2 usercontrol? Why don't just put the button directly to the DataTemplate inside TilePanel class?
If you need to reuse the template, you can put the template to resource dictionary.
If you need some special presentation code in the Tilev2 codebehind or you need to use the Tilev2 without viewmodel, it's better to create custom control instead of usercontrol in this case. it has much better design time support, and writing control templates it's easier (Triggers, DataTriggers, TempalteBinding, etc). If you used custom Control insead UserControl, you wouldn't have to write {Binding ElementName=Tile, Path=TileClickCommand}, or use RelativeSource, etc.
Third, it seems like you forced MVVM pattern where you can't really take advantage of it. Point of MVVM is separate application logic from presentation. But your Tile and TilePanel usercontrols are just presentation. You application logic could be in StartScreen which is concrete usage of TileName.
I would create custom controls called TilePanel (potentionally inherited from ItemsControl, Selector or ListBox) and if needed also for Tile. Both controls should not be aware of any viewmodels. There's absolutelly no need for that.
Take ListBox as an example. ListBox does not have viewmodel but can be easily used in MVVM scenarios. Just because ListBox it is not tied to any viewmodel, it can be databound to anything.
Just like ListBox creates ListBoxItems, or
Combobox creates ComboBoxItems, or
DataGrid creates DataGridRows or
GridView (in WinRT) creates GridViewRow, your TilePanel could create Tiles.
Bindings to tile specific properties, like Icon or Command could be specified in TilePanel.ItemContainerStyle orusing simillar appriach like DisplayMemberPath, resp ValueMemberPath in ListBox.
final usage could the look like:
<TilePanel ItemsSource="{Bidning ApplicationTiles}" />
or
<TilePanel>
<Tile Icon=".." Command=".." Text=".." />
<Tile Icon=".." Command=".." Text=".." />
</TilePanel>
Last, the name `TilePanel' evoked that it is some kind of panel like StackPanel, WrapPanel, etc. In other words, it is FrameworkElement inherited from Panel.
TilesView would be more suitable name for the control than TilePanel. The -View postfix is not from MVVM, it just follows naming convention -GridView, ListView...
Saw the problem...
To pass a parameter from button, I used CommandParameter so I could use it in switch-case scenario to know which button was clicked. But still, param was still null...
<Button x:Name="btnTile" Style="{StaticResource TileStyleButton}" CommandParameter="{Binding}" Command="{Binding Path=TileClickCommand, ElementName=Tile}" >
</Button>
TileCommand = new MyCommand() { CanExecuteFunc = param => CanExecuteCommand(), ExecuteFunc = param => Tile_TileClick(param)}
After 2 whole damn days, I changed it:
From this:
<UserControl Name="Tile"...>
<Button x:Name="btnTile" Style="{StaticResource TileStyleButton}" CommandParameter="{Binding Tag, ElementName=Tile}" Command="{Binding Path=TileClickCommand, ElementName=Tile}" >
</Button>
</UserControl>
To this:
<UserControl Name="Tile"...>
<Button x:Name="btnTile" Style="{StaticResource TileStyleButton}" CommandParameter="{Binding}" Command="{Binding Path=TileClickCommand, ElementName=Tile}" >
</Button>
</UserControl>
My first post does error because CommandParameter does not know where to get its DataContext so I replaced it to CommandParameter={Binding} so it will get whatever from the DataContext.

Binding to static field after deserializing data from an xml file - Windows Store App

I am getting data from an XML file into a class called ExerciseTable using the "INotifyPropertyChanged". And I want to bind my data to a static TextBlock without using any ListView or GridView ItemsSource.
This is my class:
public class ExerciseTable : INotifyPropertyChanged
{
int questionsNum;
bool feedback;
bool randomAnswers;
public int QuestionsNum
{
get { return questionsNum; }
set
{
questionsNum = value;
RaisePropertyChanged("QuestionsNum");
}
}
public bool Feedback
{
get { return feedback; }
set
{
feedback = value;
RaisePropertyChanged("Feedback");
}
}
public bool RandomAnswers
{
get { return randomAnswers; }
set
{
randomAnswers = value;
RaisePropertyChanged("RandomAnswers");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
This is how I'm deserializing my XML file:
string XMLPathEx = System.IO.Path.Combine(Package.Current.InstalledLocation.Path, "Assets/TableOfExercises.xml");
XDocument loadedEx = XDocument.Load(XMLPathEx);
//retrieving data from xml using LINQ
var exercise = from query in loadedEx.Descendants("exercise")
select new ExerciseTable
{
QuestionsNum = int.Parse(query.Element("settings").Attribute("qNum").Value),
Feedback = System.Convert.ToBoolean(query.Element("settings").Attribute("feedback").Value),
RandomAnswers = System.Convert.ToBoolean(query.Element("settings").Attribute("randomAnswers").Value)
};
And this is how I am binding to my TextBlock:
<TextBlock x:Name="NumberQuestion" Text="{Binding ExerciseTable.QuestionsNum}"/>
The real question is going to be how you set the data context of the textblock. The reason you say "without using any listview" makes it clear to me that you are not 100% sure how you can set the datacontext without a repeater control like listview. Allow me to show you four ways.
Instantiating it in XAML
With this code:
None
With this XAML:
<TextBlock Text="{Binding QuestionsNum}">
<TextBlock.DataContext>
<models:ExerciseTable />
</TextBlock.DataContext>
</TextBlock>
Accessing it in XAML
With this code:
public class Locator {
public ExcersizeTable CurrentTable {
get {
return new ExcersizeTable();
}
}
}
With this XAML:
<TextBlock Text="{Binding QuestionsNum}">
<TextBlock.DataContext>
<Binding Path="CurrentTable">
<Binding.Source>
<models:Locator />
</Binding.Source>
</Binding>
</TextBlock.DataContext>
</TextBlock>
Setting datacontext in code-behind
With this code:
public class MainPage: Page {
public MainPage() {
Loaded += MainPage_Loaded;
}
public void MainPage_Loaded() {
MyTextBlock.DataContext = new ExcercizeTable();
}
}
With this XAML:
<TextBlock x:Name="MyTextBlock" Text="{Binding QuestionsNum}" />
Setting text in code-behind
With this code:
public class MainPage: Page {
public MainPage() {
Loaded += MainPage_Loaded;
}
public void MainPage_Loaded() {
MyTextBlock.Text = new ExcercizeTable().Text;
}
}
With this XAML:
<TextBlock x:Name="MyTextBlock" />
Each of those approaches is the right way to do it, depending on the details of your project. Most developers use #4 because they do not understand data binding. Typically, I use a higher-level datacontext, something like this:
<Grid>
<TextBlock.DataContext>
<Binding Path="CurrentTable">
<Binding.Source>
<models:Locator />
</Binding.Source>
</Binding>
</TextBlock.DataContext>
<TextBlock Text="{Binding QuestionsNum}" />
<TextBlock>
<Run Text="Feedback:" />
<Run Text="{Binding Feedback}" />
</TextBlock>
</Grid>
Note how this approach allows you to bind to two properties while setting the datacontext only one time. This is because datacontext is a special type of property in XAML called a dependency property. One neat feature of this particular dependency property is that its values flows down through the XAML tree to every child element that doesn't explicitly set their own datacontext.
This allows you to set the datacontext property of an element very high in the XAML tree and reuse it in child elements without having to re-declare it.
How does a XAML repeater like listview do it? The exact same, just under the covers. As it repeats itself for each record in the list, it create a new child XAML tree and sets the datacontext to the individual element it is currently processing. Then your data binding works.
Setting the datacontext on your own is a fine way to build interfaces in XAML. That being said, all my samples set the datacontext to the model. This works, but is not typically the suggested approach. Most developers wrap their model(s) in a containing class we call a view-model. The view-model is what most developers set their datacontext value to.
This is not the right place to teach MVVM, but I think you should research it. http://bing.com works just fine and there are many blogs and videos out there teaching MVVM. It's a very simple design pattern and ideally suited for XAML applications.
I hope this answers your question.
Best of luck!

How to create context-aware ListBoxItem template?

I want to create XAML chat interface that will display messages differently depending on it's neighbours. Here's an example:
I think ListBox control is most suitable for this. I'm also thinking about different controls such as FlowDocumentReader but I've never used them. Also I need to mention that message's text should be selectable (across multiple messages) and I don't know how to achieve this with ListBox.
Update: The main point there is that if one side (viking in this case) send some messages in a row, the interface should concatenate those (use slim message header instead of full one). So, the look of message with header depends on whether previous message was sent by the same person.
If you were just interested in the formatting of the Headers (full or small) then a ListBox/ListView/ItemsControl with PreviousData in the RelativeSource binding is the way to go (as pointed out by anivas).
But since you added that you wanted to support for selection across multiple messages then this pretty much rules out ItemsControl and the classes that derives from it as far as I know. You'll have to use something like a FlowDocument instead.
Unfortunately FlowDocument doesn't have the ItemsSource property. There are examples of workarounds for this, like Create Flexible UIs With Flow Documents And Data Binding but this implementation pretty much makes my VS2010 crash (I didn't investigate the reason for this, might be an easy fix).
Here is how I would do it
First you design the Blocks of the FlowDocument in the designer and when you're satisfied you move them to a resource where you set x:Shared="False". This will enable you to create multiple instances of the resource instead of using the same one over and over. Then you use an ObservableCollection as the "source" for the FlowDocument and subscribe to the CollectionChanged event, and in the eventhandler you get a new instance of the resource, check if you want the full or small header, and then add the blocks to the FlowDocument. You could also add logic for Remove etc.
Example implementation
<!-- xmlns:Collections="clr-namespace:System.Collections;assembly=mscorlib" -->
<Window.Resources>
<Collections:ArrayList x:Key="blocksTemplate" x:Shared="False">
<!-- Full Header -->
<Paragraph Name="fullHeader" Margin="5" BorderBrush="LightGray" BorderThickness="1" TextAlignment="Right">
<Figure HorizontalAnchor="ColumnLeft" BaselineAlignment="Center" Padding="0" Margin="0">
<Paragraph>
<Run Text="{Binding Sender}"/>
</Paragraph>
</Figure>
<Run Text="{Binding TimeSent, StringFormat={}{0:HH:mm:ss}}"/>
</Paragraph>
<!-- Small Header -->
<Paragraph Name="smallHeader" Margin="5" TextAlignment="Right">
<Run Text="{Binding TimeSent, StringFormat={}{0:HH:mm:ss}}"/>
</Paragraph>
<!-- Message -->
<Paragraph Margin="5">
<Run Text="{Binding Message}"/>
</Paragraph>
</Collections:ArrayList>
</Window.Resources>
<Grid>
<FlowDocumentScrollViewer>
<FlowDocument Name="flowDocument"
FontSize="14" FontFamily="Georgia"/>
</FlowDocumentScrollViewer>
</Grid>
And the code behind could be along the following lines
public ObservableCollection<ChatMessage> ChatMessages
{
get;
set;
}
public MainWindow()
{
InitializeComponent();
ChatMessages = new ObservableCollection<ChatMessage>();
ChatMessages.CollectionChanged += ChatMessages_CollectionChanged;
}
void ChatMessages_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
ArrayList itemTemplate = flowDocument.TryFindResource("blocksTemplate") as ArrayList;
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (ChatMessage chatMessage in e.NewItems)
{
foreach (Block block in itemTemplate)
{
bool addBlock = true;
int index = ChatMessages.IndexOf(chatMessage);
if (block.Name == "fullHeader" &&
(index > 0 && ChatMessages[index].Sender == ChatMessages[index - 1].Sender))
{
addBlock = false;
}
else if (block.Name == "smallHeader" &&
(index == 0 || ChatMessages[index].Sender != ChatMessages[index - 1].Sender))
{
addBlock = false;
}
if (addBlock == true)
{
block.DataContext = chatMessage;
flowDocument.Blocks.Add(block);
}
}
}
}
}
And in my sample, ChatMessage is just
public class ChatMessage
{
public string Sender
{
get;
set;
}
public string Message
{
get;
set;
}
public DateTime TimeSent
{
get;
set;
}
}
This will enable you to select text however you like in the messages
If you're using MVVM you can create an attached behavior instead of the code behind, I made a sample implementation of a similar scenario here: Binding a list in a FlowDocument to List<MyClass>?
Also, the MSDN page for FlowDocument is very helpful: http://msdn.microsoft.com/en-us/library/aa970909.aspx
Assuming your ItemTemplate is a StackPanel of TextBlock header and TextBlock message you can use a MultiBinding Visibility Converter to hide the header as:
<TextBlock Text="{Binding UserName}">
<TextBlock.Visibility>
<MultiBinding Converter="{StaticResource headerVisibilityConverter}">
<Binding RelativeSource="{RelativeSource PreviousData}"/>
<Binding/>
</MultiBinding>
</TextBlock.Visibility>
</TextBlock>
And the IMultiValueConverter logic goes something like:
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var previousMessage = values[0] as MessageItem;
var currentMessage = values[1] as MessageItem;
if ((previousMessage != null) && (currentMessage != null))
{
return previousMessage.UserName.Equals(currentMessage.UserName) ? Visibility.Hidden : Visibility.Visible;
}
return Visibility.Visible;
}
Try to give a hint pseudocode like:
public abstract class Message {/*Implementation*/
public enum MessageTypeEnum {Client, Viking, None};
public abstract MessageTypeEnum MessageType {get;}
}
public class ClientMessage : Message {
/*Client message concrete implementation.*/
public override MessageTypeEnum MessageType
{
get {
return MessageTypeEnum.Client;
}
}
}
public class VikingMessage : Message
{
/ *Viking message concrete implementation*/
public override MessageTypeEnum MessageType
{
get {
return MessageTypeEnum.Viking;
}
}
}
After this in yor bindind code in XAML on binding control use XAML attribute Converter
Where you can assign a class reference which implements IValueConverter. Here are the links
Resource on web:
Converter
There you can converts the type between your UI/ModelView.
Hope this helps.
I don't think you can do this purely through XAML, you're going to need to have code written somewhere to determine the relationship between each message, i.e., is the the author of message n - 1 the same as n?
I wrote a very quick example that resulted in the desired output. My example and the resulting code snippets are in no way production level code, but it should at least point you in the right direction.
To start, I first created a very simple object to represent the messages:
public class ChatMessage
{
public String Username { get; set; }
public String Message { get; set; }
public DateTime TimeStamp { get; set; }
public Boolean IsConcatenated { get; set; }
}
Next I derived a collection from ObservableCollection to handle determining relationships between each message as they're added:
public class ChatMessageCollection : ObservableCollection<ChatMessage>
{
protected override void InsertItem(int index, ChatMessage item)
{
if (index > 0)
item.IsConcatenated = (this[index - 1].Username == item.Username);
base.InsertItem(index, item);
}
}
This collection can now be exposed by your ViewModel and bound to the ListBox in your view.
There are many ways to display templated items in XAML. Based on your example interface, the only aspect of each item changing is the header so I figured it made the most sent to have each ListBoxItem display a HeaderedContentControl that would show the correct header based on the IsConcatenated value:
<ListBox ItemsSource="{Binding Path=Messages}" HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type m:ChatMessage}">
<HeaderedContentControl Header="{Binding}">
<HeaderedContentControl.HeaderTemplateSelector>
<m:ChatHeaderTemplateSelector />
</HeaderedContentControl.HeaderTemplateSelector>
<Label Content="{Binding Path=Message}" />
</HeaderedContentControl>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
You'll notice that I am specifying a HeaderTemplateSelector which is responsible for choosing between one of two header templates:
public sealed class ChatHeaderTemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var chatItem = item as ChatMessage;
if (chatItem.IsConcatenated)
return ((FrameworkElement)container).FindResource("CompactHeader") as DataTemplate;
return ((FrameworkElement)container).FindResource("FullHeader") as DataTemplate;
}
}
And finally, here are the two header templates which are defined as resources of the view:
<DataTemplate x:Key="FullHeader">
<Border
Background="Lavender"
BorderBrush="Purple"
BorderThickness="1"
CornerRadius="4"
Padding="2"
>
<DockPanel>
<TextBlock DockPanel.Dock="Left" Text="{Binding Path=Username}" />
<TextBlock DockPanel.Dock="Right" HorizontalAlignment="Right" Text="{Binding Path=TimeStamp, StringFormat='{}{0:HH:mm:ss}'}" />
</DockPanel>
</Border>
</DataTemplate>
<DataTemplate x:Key="CompactHeader">
<Border
Background="Lavender"
BorderBrush="Purple"
BorderThickness="1"
CornerRadius="4"
HorizontalAlignment="Right"
Padding="2"
>
<DockPanel>
<TextBlock DockPanel.Dock="Right" HorizontalAlignment="Right" Text="{Binding Path=TimeStamp, StringFormat='{}{0:HH:mm:ss}'}" />
</DockPanel>
</Border>
</DataTemplate>
Again, this example is not perfect and is probably just one of many that works, but at least it should point you in the right direction.

Categories