How can I create a DataTemplate for a collection? - wpf

I would like to be able to create a DataTemplate to be used when a collection is passed into a control.
I am building a single control, that when passed an object, or a collection of objects, the view of the user control conforms to the template defined for the object type.
For example, this is a user control I have, that switches views when an object is passed into the .Content property.
<UserControl x:Class="Russound.Windows.UI.UserControls.MAX.OMS_Main_Screen.OMSContextSwitcher"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Entities="clr-namespace:Russound.Components.ReturnAuthorization.Entities;assembly=Russound.Components"
xmlns:Return_Authorization="clr-namespace:Russound.Windows.UI.UserControls.Return_Authorization"
xmlns:CallLog="clr-namespace:Russound.Windows.UI.UserControls.CallLog"
xmlns:Entities1="clr-namespace:Russound.Components.Membership.Entities;assembly=Russound.Components"
xmlns:Membership="clr-namespace:Russound.Windows.UI.UserControls.Membership"
xmlns:Entities2="clr-namespace:Russound.Components.Commerce.MAX.Entities;assembly=Russound.Components"
xmlns:OMS_Main_Screen="clr-namespace:Russound.Windows.UI.UserControls.MAX.OMS_Main_Screen"
xmlns:Entities3="clr-namespace:Russound.Components.CallLog.Entities;assembly=Russound.Components"
MinHeight="600" MinWidth="700">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Russound.Windows;component/UI/RussoundDictionary.xaml"/>
</ResourceDictionary.MergedDictionaries>
<DataTemplate DataType="{x:Type Entities3:Case}" >
<CallLog:CaseReadOnlyDisplay DataContext="{Binding}" />
</DataTemplate>
<DataTemplate DataType="{x:Type Entities:RAMaster}">
<Return_Authorization:RaMasterUi DataContext="{Binding}" />
</DataTemplate>
<DataTemplate DataType="{x:Type Entities1:RussUser}">
<Membership:CMCControlWpf DataContext="{Binding}" />
</DataTemplate >
<DataTemplate DataType="{x:Type Entities2:MaxCustomer}">
<OMS_Main_Screen:MaxCustomerConfigWpf DataContext="{Binding}" />
</DataTemplate >
</ResourceDictionary>
</UserControl.Resources>
</UserControl>
I would like to be able to do something like:
<DataTemplate DataType="{x:Type IEnumerable<MaxCustomer>}">
<OMS_Main_Screen:MaxCustomerConfigWpf DataContext="{Binding}" />
</DataTemplate >
but I always get a compiler error, so I am at somewhat of a loss.

You can create a typed collection and use that type instead of the IEnumerable directly
public class MyCollection:IEnumerable<MaxCustomer>
{
....
}
<DataTemplate DataType="{x:Type Entities:MyCollection}">
<OMS_Main_Screen:MaxCustomerConfigWpf DataContext="{Binding}" />
</DataTemplate >

Related

MvvmCross: View inside another View (or the equivalent to a CaliburnMicro Conductor)

I am pretty new to MvvmCross and the mvvm pattern in general, so I started a small learning project and immidiatly ran into a wall. I based my application on the idea of having a MainView which contains a standard Menu and a child MvxWpfView. This ChildView should be a simple ReadMeView first, but on user input it should switch to an other View (the one with actual data on it). I already found a few articles about this issue but none of them worked or i wasn't able to follow.
My setup:
Core Library (.NET Standard 2.0)
Wpf app (.NET Core 3.1)
This is my MainViewModel with this users solution still implemented:
using MvvmCross.Commands;
using MvvmCross.ViewModels;
namespace puRGE.Core.ViewModels
{
public class MainViewModel : MvxViewModel
{
#region Fields -------------------------------------------------------------------------------------
private HomeViewModel m_homeViewModel = new HomeViewModel();
#endregion ------------------------------------------------------------------------ Fields endregion
#region Properties ---------------------------------------------------------------------------------
public HomeViewModel Home {
get => m_homeViewModel;
set => SetProperty(ref m_homeViewModel, value);
}
#endregion -------------------------------------------------------------------- Properties endregion
#region Constructors -------------------------------------------------------------------------------
public MainViewModel() { }
#endregion ------------------------------------------------------------------ Constructors endregion
#region Public methods -----------------------------------------------------------------------------
public override void Prepare()
{
Home = new HomeViewModel();
}
#endregion ---------------------------------------------------------------- Public methods endregion
}
}
This is the xaml part located inside my MainView (still part of this users solution):
<Menu>
<!-- Some MenuItems -->
</Menu>
<UserControl DataContext="{Binding Home, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
Vision VS. Reality
image of what I am trying to achieve
I also tried using the MvxContentPresentation attribute, but to be honest I lost myself somewhere in the MvvmCross Documentation and at this point I am almost stepping on my eye bags.
<local:HomeView/>
This doesn't work either. Bindings stop working this way even when the properties value get's set inside the ViewModels Prepare()method. I guess calling the View like this breaks some chain of events or something.
How do I place a View inside my MainView? Is this Child then able to navigate to another View and vice versa (following the Navigation Documentation)?
Edit_01102020:
A general Mvvm approach doesn't seem to work so far.
Edit_02102020:
Home can now navigate to SomeOtherViewModel and back. Still no clue how to contain this in my MainView.
In your MainView.xaml:
<Menu>
<!-- Some MenuItems -->
</Menu>
<UserControl DataContext="{Binding Home}">
<UserControl.Resources>
<DataTemplate DataType="{x:Type viewModels:ModelAViewModel}">
<local:ModelAView />
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:ModelBViewModel}">
<local:ModelBView />
</DataTemplate>
</UserControl.Resources>
<ContentPresenter Content="{Binding}" />
</UserControl>
Remember to Notify your changes in your HomeViewModel:
public void ActivateModelAViewModel()
{
HomeViewModel = Mvx.IoCProvider.Resolve<ModelAViewModel>();
//In your HomeViewModel property:
//RaisePropertyChanged(() => HomeViewModel);
}
public void ActivateModelBViewModel()
{
HomeViewModel = Mvx.IoCProvider.Resolve<ModelBViewModel>();
//In your HomeViewModel property:
//RaisePropertyChanged(() => HomeViewModel);
}
I have also found that the following alternatives also do the job (except for the first one, commented out):
<StackPanel>
<!--<TextBlock FontWeight="Bold">1. ContentPresenter - DataContext</TextBlock>
<ContentPresenter DataContext="{Binding ActiveViewModel}">
<ContentPresenter.Resources>
<DataTemplate DataType="{x:Type viewModels:WorkWeekViewModel}">
<local:WorkWeekView />
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:WorkViewModel}">
<local:WorkView />
</DataTemplate>
</ContentPresenter.Resources>
</ContentPresenter>-->
<TextBlock FontWeight="Bold">2. ContentPresenter - Content</TextBlock>
<ContentPresenter Content="{Binding ActiveViewModel}" >
<ContentPresenter.Resources>
<DataTemplate DataType="{x:Type viewModels:WorkWeekViewModel}">
<local:WorkWeekView />
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:WorkViewModel}">
<local:WorkView />
</DataTemplate>
</ContentPresenter.Resources>
</ContentPresenter>
<TextBlock FontWeight="Bold">3. ContentControl - DataContext</TextBlock>
<ContentControl DataContext="{Binding ActiveViewModel}">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type viewModels:WorkWeekViewModel}">
<local:WorkWeekView />
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:WorkViewModel}">
<local:WorkView />
</DataTemplate>
</ContentControl.Resources>
<ContentPresenter Content="{Binding}" />
</ContentControl>
<TextBlock FontWeight="Bold">4. ContentControl - Content</TextBlock>
<ContentControl Content="{Binding ActiveViewModel}">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type viewModels:WorkWeekViewModel}">
<local:WorkWeekView />
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:WorkViewModel}">
<local:WorkView />
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
<TextBlock FontWeight="Bold">5. UserControl - DataContext</TextBlock>
<UserControl DataContext="{Binding ActiveViewModel}">
<UserControl.Resources>
<DataTemplate DataType="{x:Type viewModels:WorkWeekViewModel}">
<local:WorkWeekView />
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:WorkViewModel}">
<local:WorkView />
</DataTemplate>
</UserControl.Resources>
<ContentPresenter Content="{Binding}" />
</UserControl>
<TextBlock FontWeight="Bold">6. UserControl - Content</TextBlock>
<UserControl Content="{Binding ActiveViewModel}">
<UserControl.Resources>
<DataTemplate DataType="{x:Type viewModels:WorkWeekViewModel}">
<local:WorkWeekView />
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:WorkViewModel}">
<local:WorkView />
</DataTemplate>
</UserControl.Resources>
</UserControl>
<TextBlock FontWeight="Bold">7. MvxWpfView - DataContext</TextBlock>
<views:MvxWpfView DataContext="{Binding ActiveViewModel}">
<views:MvxWpfView.Resources>
<DataTemplate DataType="{x:Type viewModels:WorkWeekViewModel}">
<local:WorkWeekView />
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:WorkViewModel}">
<local:WorkView />
</DataTemplate>
</views:MvxWpfView.Resources>
<ContentPresenter Content="{Binding}" />
</views:MvxWpfView>
<TextBlock FontWeight="Bold">8. MvxWpfView - Content</TextBlock>
<views:MvxWpfView Content="{Binding ActiveViewModel}">
<views:MvxWpfView.Resources>
<DataTemplate DataType="{x:Type viewModels:WorkWeekViewModel}">
<local:WorkWeekView />
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:WorkViewModel}">
<local:WorkView />
</DataTemplate>
</views:MvxWpfView.Resources>
</views:MvxWpfView>
</StackPanel>
Some are just inherited from the others, eg. MvxWpfView : UserControl; UserControl : ContentControl, but not sure about ContentPresenter, which needs not to have the child <ContentPresenter Content="{Binding}" /> inside (since it is itself a ContentPresenter).
In my case ActiveViewModel, WorkWeekViewModel and WorkViewModel are instances of MvxViewModel; WorkWeekView and WorkView are instances of MvxWpfView.

StaticResource found when added during runtime, but not on launch

I have this parent control that displays its CurrentViewModel, and instance of ViewModelWithExam, using the data templates provided.
<UserControl x:Class="Gui.Views.ExamsTabView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:views="clr-namespace:Gui.Views.Tabs.ExamsTabViews"
xmlns:viewmodels="clr-namespace:Gui.ViewModels"
xmlns:data="clr-namespace:DataManagement;assembly=DataManagement"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<DataTemplate DataType="{x:Type viewmodels:ExamInfoViewModel}">
<views:ExamInfoView/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:ExamAcquireImageViewModel}">
<views:ExamAcquireImageView/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:ExamEditImageViewModel}">
<views:ExamEditImageView/>
</DataTemplate>
<!--Reuseable control to display the simple exam header-->
<DataTemplate x:Key="ExamHeader" DataType="{x:Type data:Exam}">
<views:ExamHeaderView/>
</DataTemplate>
</UserControl.Resources>
<Grid VerticalAlignment="Stretch" Margin="20">
<ContentControl Content="{Binding CurrentViewModel}"/>
</Grid>
</UserControl>
Any of the views can use the ExamHeader to display their Exam property:
<ContentControl ContentTemplate="{StaticResource ExamHeader}" Content="{Binding Exam}"/>
The ExamHeader template, however, exhibits odd behavior. If I remove the x:Key property from the template, and remove the ContentTemplate property from the ContentControl, and then launch the app, the header displays, as expected.
While the app is running, if I add back the x:Key, the header disappears and is replaced by the classname of the Exam. As expected. If I then add back the ContentTemplate to point to the header template, the header template shows. It works.
However, if I launch the app with the x:Key and ContentTemplate in place, then loading a page with a header puts the application in break mode, saying "The resource 'ExamHeader' could not be found." Indeed, when the app isn't running, this error appears under the ControlTemplate property.
But, to say again, if I add in the x:Key and ContentTemplate while the app is running, it work fine!
How do I fix this?
It might be a resource ordering issue. Try moving the ExamHeader resource to the top of your resources:
<UserControl.Resources>
<!--Reuseable control to display the simple exam header-->
<DataTemplate x:Key="ExamHeader" DataType="{x:Type data:Exam}">
<views:ExamHeaderView/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:ExamInfoViewModel}">
<views:ExamInfoView/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:ExamAcquireImageViewModel}">
<views:ExamAcquireImageView/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:ExamEditImageViewModel}">
<views:ExamEditImageView/>
</DataTemplate>
</UserControl.Resources>

Sharing DataTemplates between controls

I have a ContentPresenter in a couple of places in my application with exactly the same DataTemplates. For now I simply copy-pasted them, but I'd like to clean that up and share them between ContentPresenter instances. I tried this approach:
<ContentControl Content="{Binding DataEditorViewModel}">
<ContentControl.Resources>
<ResourceDictionary Source="pack://application:,,,/LogAnalyzer;component/PredicateDataEditors.xaml" />
</ContentControl.Resources>
</ContentControl>
Application runs, but DataTemplates aren't being applied, I simply see name of class being ContentPresenter's content instead of defined template. I put templates in ResourceDictionary in the following way:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:LogAnalyzer"
xmlns:p="clr-namespace:LogAnalyzer.BusinessLogic.ViewModels.Processing;assembly=LogAnalyzer.BusinessLogic"
xmlns:xwt="http://schemas.xceed.com/wpf/xaml/toolkit">
<DataTemplate DataType="{x:Type p:MessageRuleDataEditorViewModel}" x:Key="{x:Type p:MessageRuleDataEditorViewModel}">
(...)
</DataTemplate>
(...)
</ResourceDictionary>
What should I do to embed DataTemplates correctly in ContentPresenter's resources?
if you define this data template in your app.xaml:
<DataTemplate DataType="{x:Type p:MessageRuleDataEditorViewModel}">
<TextBlock Text="testing" />
</DataTemplate>
and then do something like:
<ListBox ItemsSource="{Binding MyCollectionOfMessageRuleDataEditorViewModels}"/>
then your data template is automatically applied to every object of that same type. dont define a key for your global templates

How to use static resources for automatic DataTemplate resolution in ListView

I have
<ListView ItemsSource="{Binding Path=Fruit}">
<ListView.Resources>
<DataTemplate DataType="Apple">
<TextBlock Text="This is an apple"/>
</DataTemplate>
<DataTemplate DataType="Orange">
<TextBlock Text="This is an orange"/>
</DataTemplate>
<DataTemplate DataType="Potato">
<TextBlock Text="Lets assume potato is a fruit"/>
</DataTemplate>
</ListView.Resources>
where Fruit is
ObservableCollection<IFruit> Fruit = new Observable Collection<IFruit>();
public class Apple:IFruit{ }
public class Orange:IFruit{ }
public class Potato:IFruit{ }
This works fine. But since all of the seperate Fruit markups are quite large, I'd rather move their DataTemplates to their own ResourceDictionaryies in seperate files.
What I am trying to do is
<ListView ItemsSource="{Binding Path=Fruit}">
<ListView.Resources>
<StaticResource ResourceKey="AppleDataTemplate" />
<StaticResource ResourceKey="OrangeDataTemplate" />
<StaticResource ResourceKey="PotatoDataTemplate" />
</ListView.Resources>
where DataTemplates are
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
xmlns:models="clr-namespace:MyApp.Models">
<DataTemplate x:Key="AppleDataTemplate" DataType="Apple">
<TextBlock Text="This is an apple"/>
</DataTemplate>
</ResourceDictionary>
but this throws
{"'XAML Node Stream: Value of 'System.Windows.Markup.StaticResourceHolder' must follow a StartObject and StartMember.' Line number '130' and line position '18'."}
Where Line number '130' is the <StaticResource> element.
My question then is how does one use static resources for automatic DataTemplate resolution in a ListView?
According to MSDN, shouldn't the StartObject and StartMember be implicit for those elements? Similar to how <Party.Favors> is defined in the documentation?
You could merge those DataTemplate ResourceDictionarys into ListView.Resources by doing:
<ListView ItemsSource="{Binding Path=Fruit}">
<ListView.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="AppleTemplate.xaml" />
<ResourceDictionary Source="OrangeTemplate.xaml" />
<ResourceDictionary Source="PotatoTemplate.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</ListView.Resources>
</ListView>
Then, the DataTemplates will be available to ListView. Alternatively, you can merge these ResourceDictionarys at a higher level (i.e. the UserControl XAML file where you have your ListView defined).
You may want to remove x:Key from your DataTemplate definitions in ResourceDictionarys.

DataTemplate-driven View injection with MVVM

I have a container view that looks something like this
<UserControl x:Class="Views.ContainerView">
<UserControl.Resources>
<ResourceDictionary>
<DataTemplate DataType="{x:Type viewmodels:AViewModel}">
<views:MyView />
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:BViewModel}">
<views:MyView />
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:CViewModel}">
<views:MyView />
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:DViewModel}">
<views:MyView />
</DataTemplate>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<ListBox ItemsSource="{Binding Path=AvailableViewModels}"
SelectedItem="{Binding Path=CurrentViewModel}"
IsSynchronizedWithCurrentItem="True" />
<ContentControl Content="{Binding Path=CurrentViewModel}" />
</Grid>
</UserControl>
All my viewmodels inherit BaseViewModel so I turned my view into this
<UserControl x:Class="Views.ContainerView">
<UserControl.Resources>
<ResourceDictionary>
<DataTemplate DataType="{x:Type viewmodels:BaseViewModel}">
<views:MyView />
</DataTemplate>
</ResourceDictionary>
</UserControl.Resources>
<StackPanel>
<ListBox ItemsSource="{Binding Path=AvailableViewModels}"
SelectedItem="{Binding Path=CurrentViewModel}"
IsSynchronizedWithCurrentItem="True" />
<ContentControl Content="{Binding Path=CurrentViewModel}" />
</StackPanel>
</UserControl>
thinking it would instantiate just a single MyView and just rebind the viewmodel when ListBox.SelectedItem changes. Am I understanding this behavior correctly? Is this a preferred practice? How can I verify that I'm not churning memory as I switch between views?
To expand on Pavel's answer and clarify what happens when to the views when you change view-models is that a new view will be generated for the new view-model and the old view will hopefully be garbage collected in time.
The problem with this is sometimes we will have views that registers to some event in code behind (non-weak events) and this will prevent the view from being collected and you will have memory leaks.
Two approaches.
Any event subscription in view code-behind should be weaken (EventAggregator in PRISM) to allow garbage collection.
Register an instance of the view with the unity container and resolve it when you need to reuse it. Before you inject it into the region, just update the DataContext.
Hope this helps.
It will instantiate a new MyView for each view model you use. If you want to reuse your user controls, you can set the DataContext property on each user control.

Resources