I'm trying to inherit MyTabItem from System.Windows.Controls.TabItem class. The problem is, that original TabItem has properties of generic object type:
public object Header;
public object Content;
I'm trying to hide those properties in my derived class with different types.
public class MyTabItem: TabItem
{
public new MyTabHeader Header;
public new MyTabContent Content;
}
This way, I can access MyTabItem.Header and MyTabItem.Content without type casting.
The idea is pretty decent and code compiles correctly. However when application starts I see empty controls (no error is reported). When I remove those lines and use base class properties, it works fine.
Of course I could add two additional properties which would internally return casted (MyTabHeader)Header or (MyTabContent)Header, but it seems to be a little redundant.
I'm asking if there is any other way to correctly implement those properties, so they actually work in my application.
This runs completely counter to how WPF was designed to be used. Your XAML objects are supposed to be loosely bound to data, in the vast majority of cases you shouldn't even need to create a custom control. The fact that you are doing this, and then trying to replace the members with type-safe versions of your own, means your view code and your view logic code are no longer separated, and that is going to create you a world of headache down the track.
If you need dynamic tabbing then one way to do it is to first declare an abstract class representing your pages and to derive your page types from it:
public interface IBasePage
{
string Header { get; }
}
public class MyPageA : ViewModelBase, IBasePage
{
public string Header { get { return "Page A"; } }
}
public class MyPageB : ViewModelBase, IBasePage
{
public string Header { get {return "Page B";} }
}
public class MyPageC : ViewModelBase, IBasePage
{
public string Header { get {return "Page C";} }
}
Your view model (which is what your window DataContext should be set to) should then contain a collection of the tabbed pages you wish to display:
public class MyViewModel : ViewModelBase
{
private IEnumerable<IBasePage> _MyPages = new List<IBasePage>(){
new MyPageA(),
new MyPageB(),
new MyPageC()
};
public IEnumerable<IBasePage> MyPages {get {return this._MyPages;}}
}
The tab control in your XAML is then loosely bound to this and should contain a style for your TabItem (so it know what text to use for the header etc) and DataTemplates so that it knows how to render each of the page types you've created:
<TabControl ItemsSource="{Binding MyPages}" SelectedItem="{Binding MyPages[0], Mode=OneTime}">
<TabControl.Resources>
<!-- TabItem style -->
<Style TargetType="{x:Type TabItem}">
<Setter Property="Header" Value="{Binding Header}" />
</Style>
<!-- Content templates -->
<DataTemplate DataType="{x:Type local:MyPageA}">
<TextBlock Text="This is page A" />
</DataTemplate>
<DataTemplate DataType="{x:Type local:MyPageB}">
<TextBlock Text="This is page B" />
</DataTemplate>
<DataTemplate DataType="{x:Type local:MyPageC}">
<TextBlock Text="This is page C" />
</DataTemplate>
</TabControl.Resources>
</TabControl>
The end result is a regular tab control that is completely data driven and is bound to your already-strongly-typed models:
Related
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
That's how I defined TabControl:
<TabControl ItemsSource="{Binding OpenedProjects, UpdateSourceTrigger=PropertyChanged}"
SelectedItem="{Binding SelectedProject, Mode=OneWay}">
<!-- headers -->
<!-- header definition is unimportant for this question -->
<!-- content -->
<TabControl.ContentTemplate>
<DataTemplate>
<local:ProjectView />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
And these are the two methods I have defined in my Module class, that are used to register and use views:
protected override void _initializeViews() {
_container.RegisterType<MainMenuView>();
_container.RegisterType<ProjectsView>();
_container.RegisterType<ProjectView>();
_container.RegisterType<ContentView>();
}
protected override void _initializeRegions() {
IRegion menuRegion = _regionManager.Regions[RegionNames.MainMenuRegion];
IRegion projectsRegion = _regionManager.Regions[RegionNames.ProjectsRegion];
IRegion contentRegion = _regionManager.Regions[RegionNames.ContentRegion];
menuRegion.Add(_container.Resolve<MainMenuView>());
projectsRegion.Add(_container.Resolve<ProjectsView>());
contentRegion.Add(_container.Resolve<ContentView>());
}
And the View constructor:
public ProjectView(ProjectsViewModel vm) {
InitializeComponent();
DataContext = vm;
}
What I want to achieve is to inject ProjectView into TabControl's content area. Obviously, currently it doesn't work because of the ViewModel argument in the above constructor. How can I create this functionality, the PRISM way?
EDIT:
I found this: How to inject views into TabControl using Prism? however if I do the same as the author of that question, I'm getting:
System.InvalidOperationException: ItemsControl's ItemsSource property is not empty.
You TabControl didn't have a region so you can't inject something into your TabControl. Otherwise you only use simple MVVM to inject something into your view.
To use Prism to inject something in your TabControl. You only need this line:
<TabControl prism:RegionManager.RegionName="TabRegion"/>
And then you can inject something very easy into your View.
_regionManager.RequestNavigate("TabRegion", new Uri("ProjectView", UriKind.Relative));
Before that you have to add the View to your Containier with:
UnityContainer.RegisterType<object, ProjectView>("ProjectView");
To add the Headertext you can easy changed the Style of the TabItem and bind the Header to the ViewModel from the ProjectView:
<UserControl.Resources>
<Style TargetType="TabItem">
<Setter Property="Header" Value="{Binding DataContext.Name}" />
</Style>
</UserControl.Resources>
I hope that was the answer you looking for^^
The answer from #ascholz help me to implement this. Although the last step didn't work for me:
<UserControl.Resources>
<Style TargetType="TabItem">
<Setter Property="Header" Value="{Binding DataContext.Name}" />
</Style>
</UserControl.Resources>
What i did instead was:
1 - Create a Tab Control with a prism region (Inside MainWindows in my case).
<TabControl prism:RegionManager.RegionName="TabRegion"/>
2 - Create a "user control" of type TabItem (NewTabView) that holds the tab views. Note that i'm binding the Header here. The idea here would be to add a region here as well inside the grid (for the content of the tab), or to make every control that needs to be inside the tab a child of TabItem.
<TabItem
x:Class="Client.WPF.Views.NewTab"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
Header="{Binding Title}">
<Grid>
<Button Content="{Binding RandomNumber}"/>
</Grid>
</TabItem>
///The code behind should inherit from TabItem as well
public partial class NewTab : TabItem
///The viewmodel has a "Title" property
private string _title = "New Tab";
public string Title
{
get { return _title; }
set { SetProperty(ref _title, value); }
}
4 - Finally, i navigate to the NewTab like this (MainWindowViewModel code)
public DelegateCommand NewTab { get; }
public MainWindowViewModel(IRegionManager regionManager, IEventAggregator eventAggregato)
{
this.regionManager = regionManager;
this.eventAggregator = eventAggregator;
NewTab = new DelegateCommand(NewTabAction);
}
private void NewTabAction()
{
regionManager.RequestNavigate("TabRegion", "NewTab");
}
5 - As an added bonus, if you want to allow more than 1 instance of the tab, you can do something like this on the view model (NewTabViewModel).
///First add the IConfirmNavigationRequest interface
public class NewTabViewModel : BindableBase, IConfirmNavigationRequest
///...
///Then the implementation should look like this
public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
{
continuationCallback(true);///Will allow multiple instances (tabs) of this view
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
}
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return false;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
}
Or you could also add views directly to the region. Although you would need to resolve views using the IContainerProvider. Something like this:
var view = containerProvider.Resolve<NewTab>();
regionManager.Regions["TabRegion"].Add(view);
I have a WinRT project based on prism with several pages, usercontrols etc.
For some reason I need to bind several views to a single model object accessed by the the viewmodels (each one belonging to a view).
The single model object is injected by a Unity container like other objects, that need to be singleton-like as the eventaggregator for instance.
To keep things simple I made an example with only one bool variable bound to a checkbox in each view, that should be synchronized over the views.
My problem is: When I check the box in the mainpage, the checkbox in a second page is following the value when navigating to that page(UserInpuPage in the example) BUT NOT the checkbox in the UserControl place on the Mainpage.
After a debug session I saw the variables in the single model having the right values, but the GUI on the Usercontrol (MyUserControl in the example) is not updated.
A Mechanism like GetBindingExpression(...) and then UpdateTarget() like in WPF seems not to exist in the WinRT library.
For design reasons (using prism mvvm I don't want to break the concept of the autowire and dynamical instantiation of the vm's) a static context defined in the resources section of the page and/or usercontrol is not what I'm looking for.
How can I achieve the update of the checkbox in the usercontrol with the model the same way as it works for the userinputpage after navigating?
Any help would be appreciated.
// Interface mendatory to work with Unitiy-Injection for RegisterInstance<ISingletonVM>(...)
public interface ISingletonVM
{
bool TestChecked{ get; set; }
}
public class SingletonVM : BindableBase, ISingletonVM
{
bool _testChecked = false;
public bool TestChecked
{
get
{
return _testChecked;
}
set
{
SetProperty(ref _testChecked, value);
}
}
}
This is the relevant code in the viewmodels (same for every vm, but vm from usercontrol in this case):
class MyUserControlViewModel : ViewModel
{
private readonly ISingletonVM _singletonVM;
public MyUserControlViewModel(ISingletonVM singletonVM)
{
_singletonVM = singletonVM;
}
public bool TestChecked
{
get
{
return _singletonVM.TestChecked;
}
set
{
_singletonVM.TestChecked = value;
}
}
}
Relevant XAML code fragments for the three views:
MainPage:
<prism:VisualStateAwarePage x:Name="pageRoot" x:Class="HelloWorldWithContainer.Views.MainPage"...>
...
<StackPanel Grid.Row="2" Orientation="Horizontal">
<ctrl:MyUserControl ></ctrl:MyUserControl>
<CheckBox IsChecked="{Binding TestChecked, Mode=TwoWay}" Content="CheckBox" HorizontalAlignment="Left" VerticalAlignment="Top"/>
</StackPanel>
...
UserInputPage:
<prism:VisualStateAwarePage x:Name="pageRoot"
x:Class="HelloWorldWithContainer.Views.UserInputPage"
...
<CheckBox IsChecked="{Binding TestChecked, Mode=TwoWay}" Content="CheckBox" HorizontalAlignment="Left" Margin="440,190,0,0" VerticalAlignment="Top"/>
...
UserControl:
<UserControl
x:Class="HelloWorldWithContainer.Views.MyUserControl" prism:ViewModelLocator.AutoWireViewModel="True"
<Grid>
<CheckBox Content="CheckBox" IsChecked="{Binding TestChecked, Mode=TwoWay}" HorizontalAlignment="Left" VerticalAlignment="Top" Width="282"/>
</Grid>
Your user control never gets notified about changes in MyUserControlViewModel.TestChecked property and that's why the view is never updated. One thing you can do to fix this is to subscribe to your SingletonVM.PropertyChanged event in the constructor of MyUserControlViewModel. Your ISingletonVM needs to implement the INotifyPropertyChanged interface. So the constructor of MyUserControlViewModel will be something like this:
public MyUserControlViewModel(ISingletonVM singletonVM)
{
_singletonVM = singletonVM;
_singletonVM.PropertyChanged += (sender, args) => OnPropertyChanged("TestChecked");
}
I would like to create an implicit DataTemplate that works on an array or IEnumerable of my class. This way I have a template that describes how to render a bunch of items instead of just one. I want to do this so I can, among other things, show the results in a tooltip. eg
<TextBlock Text="{Binding Path=CustomerName}" ToolTip="{Binding Path=Invoices}">
The tooltip should see that Invoices is a bunch of items and use the appropriate data template. The template would look something like this:
<DataTemplate DataType="{x:Type Customer[]}">
<ListBox "ItemsSource={Binding}">
etc
That didn't work so I tried the example from this post x:Type and arrays--how? which involves creating a custom markup extension. This works if you specify the key but not for an implicit template
So then I tried making my own custom markup extension inheriting from TypeExtension like below but I get an error that says "A key for a dictionary cannot be of type 'System.Windows.Controls.StackPanel'. Only String, TypeExtension, and StaticExtension are supported." This is a really weird error as it is taking the content of the datatemplate as the key?? If I specify a key then it works fine but that largely defeats the purpose.
[MarkupExtensionReturnType(typeof(Type)), TypeForwardedFrom("PresentationFramework, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")]
public class ArrayTypeExtension
: TypeExtension
{
public ArrayTypeExtension() : base() { }
public ArrayTypeExtension(Type type) : base(type)
{
}
public ArrayTypeExtension(string value) : base(value)
{
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
Type val = base.ProvideValue(serviceProvider) as Type;
return val == null ? null : val.MakeArrayType();
}
}
As noted in the question you linked to {x:Type ns:TypeName[]} works. It may screw over the designer but at runtime it should be fine.
To avoid designer errors the template can be moved to App.xaml or a resource dictionary (or of course just don't use the designer at all).
(The error mentioning the control inside the template sounds like a bug in the code generator or compiler, sadly i doubt that there is much you can do about that one.)
If you are OK with creating your own type, I just tried and following and it is working. Create a specific type for your collection:
public class InvoiceCollection : List<Invoice> { }
public class Customer {
public string name { get; set; }
InvoiceCollection invoices { get; set; }
}
and then the XAML with data template:
<DataTemplate DataType={x:Type InvoiceCollection}>
<ListBox ItemsSource="{Binding}" />
</DataTemplate>
<TextBox Text="{Binding name}" Tooltip="{Binding invoices}" />
I have been trying to bind listbox with an observableConnection in Xaml on WP7 with no luck. All I want to do is to make listbox to show an instance of my class that inherits from ObservableConnection and apply some style on listbox. I can do this from code like
public Storage.Categories tmp;
...
tmp = new Storage.Categories();
listBox1.ItemsSource = tmp;
but how to apply style on that?
Here is code:
<ListBox Height="497"
HorizontalAlignment="Left"
Margin="0,104,0,0"
Name="listBox1"
VerticalAlignment="Top"
Width="450">
namespace Genesa.Storage
{
public class Categories : ObservableCollection<Category>
{
public void LoadCategories()
{
// deserialize obiect
}
public void SaveCategories()
{
// serialize obiect
}
public Categories() : base()
{
LoadCategories();
}
}
public class Category
{
public Category() { }
public String name { get; set; }
public String description { get; set; }
public Category(String _name, String _description)
{
name = _name;
description = _description;
}
public override string ToString()
{
return String.Format("{0} - {1}", name, description);
}
}
}
You're going to want to use a DataTemplate. A data template let's you structure the items in your ListBox. For example:
<ListBox>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding name}" />
<TextBlock Text="{Binding description}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Also, you might want to reconsider inheriting from ObservableCollection. If what you're doing is as simple as it looks above, you probably want to stick to creating a class which contains an ObservableCollection and which implements the INotifiyPropertyChanged interface. This is assuming you're using the MVVM design pattern. If you're not, feel free to disregard this suggestion. If you are implementing MVVM, you also want to make the Category class implement the INotifyPropertyChanged interface.
As Jared suggests, the most appropriate approach to your solution is to provide an ItemTemplate for the ListBox that defines the structure of each item in the ListBox, which enables you to bind directly to properties on your class, instead of having to override the ToString method. However, there is a small mistake in Jared's DataTemplate because it can only contain a single item, so you need to wrap the elements in some kind of container, as shown below:
<ListBox>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding name}" />
<TextBlock Text="{Binding description}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
You only need to implement the INotifyPropertyChanged on your Category class if the properties can change during the lifetime of that object. If the values are constant throughout it's lifetime, then there's no need.
usually the ObservableCollection is member of the ViewModel to which the View binds to. You don't have to inherit from ObservableCollection and the logic from Categories class can be placed inside ViewModel.
Then you need to set DataContext of Page or other object in hierarchy to be the ViewModel and then you can bind for example ListBox.ItemsSource to ViewModel.ObservableCollection.
After that DataTemplate will work in scope of Category (single item in ObservableCollection).
Regarding the logic of loading etc, there is usually one more layer responsible for these operations, which is injected to ViewModel, but if you don't want it, it's just fine.