i am currently learning C# and WPF. I have started coding simple application, i have
1 model class (Book),
2 model views (BookListModelView, AddNewBookModelView) and
2 views booth are UserControl (BookListView, AddNewBookView).
My question is how can i change to AddNewBookView when my current view is BookListView?
MainWindow.xaml :
<ContentControl Grid.Row="1" Grid.Column="1" Content="{Binding}"/>
MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
BookListModelView bookListMV = new BookListModelView();
this.DataContext = bookListMV;
}
}
BookListView.xaml.cs
public partial class BookListView : UserControl
{
public BookListView()
{
InitializeComponent();
}
private void NewBookBtn_Click(object sender, RoutedEventArgs e)
{
this.DataContext = new AddNewBookViewModel();
}
}
Thank you for your answers!
The usual way to achieve this is with DataTemplates. Basically you have a property in your main view model containing the view models of your child page:
public object PageViewModel {get; set;} // <--- needs property change notification
I've used object as the type here, whereas in practice you typically have some base class for your pages.
Over in your view you create a ContentControl and bind it to this property:
<ContentControl Content="{Binding PageViewModel}" />
Finally, you need to specify some kind of mapping specifying which view to display for each view model type. That's where data templates come in:
<DataTemplate DataType="{x:Type local:ChildViewModel1}">
<local:ChildViewControl1 />
</DataTemplate>
<DataTemplate DataType="{x:Type local:ChildViewModel2}">
<local:ChildViewControl2 />
</DataTemplate>
Another way to achieve this is via Data Triggers. These get put in the parent's style and check for each possible value, setting the content explicitly:
<DataTrigger Binding="{Binding PageViewModelProperty}" Value="SomeChildViewModel1Value">
<Setter Property="Template" Value="{StaticResource MyPageTemplate1}" />
</DataTrigger>
<DataTrigger Binding="{Binding PageViewModelProperty}" Value="SomeChildViewModel2Value">
<Setter Property="Template" Value="{StaticResource MyPageTemplate2}" />
</DataTrigger>
... and so on. This is a better fit in cases where you may not have a separate class type for each of your page types e.g. in the case of page containing content that is generated dynamically.
Related
I'm developing an App for Windows Phone 8.1. In that App I want to bind the Items of an ObservableCollection<DisruptionDisplayElement> to a ListView. DisruptionDisplayElement has a property named bool IsFavorite. In the ListView I want to hide all items, where IsFavorite is false.
If I do it by using ItemContainerStyle and set the Visibility-Property to collapsed by using a Converter, it is not working. If I define the Backgroundcolor the same way for testing, it works.
I can also hide the Grid, where everything of the ListViewItem is in, but in that case I still have the the decoration of the ListViewItem, that takes round about 50 pixels of space.
Here is what I've got:
XAML:
<Page
x:Class="myApp.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:myApp"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:converter="using:myApp.Converter"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
>
<Page.Resources>
<converter:BoolToVisibilityConverter x:Key="BoolToVisibilityConv"/>
</Page.Resources>
<Grid>
<Hub Header="{Binding CityName}"
SectionsInViewChanged="Hub_SectionsInViewChanged"
Grid.Row="1"
>
<HubSection Header="My Lines" Name="hubFavorites">
<DataTemplate>
<Grid Margin="0,-25,0,0">
<ListView
ItemsSource="{Binding DisruptionDisplayList}"
Grid.Row="1"
>
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<!-- This seems not to work -->
<Setter Property="Visibility" Value="{Binding IsFavorite, Converter={StaticResource BoolToVisibilityConv}}"/>
<!-- For testing -->
<Setter Property="Background" Value="Aqua"/>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<!-- The Visibility-Property is just for testing as described -->
<Grid
Margin="0,0,0,10"
Visibility="{Binding IsFavorite, Converter={StaticResource BoolToVisibilityConv}}"
>
<!-- Content here -->
<TextBlock Text="{Binding Message}"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</DataTemplate>
</HubSection>
</Hub>
</Grid>
</Page>
The Converter:
namespace myApp.Converter
{
public class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string culture)
{
return (bool) value ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, string culture)
{
throw new NotImplementedException();
}
}
}
DisruptionDisplayElement:
public class DisruptionDisplayElement
{
public string Message { get; set; }
public bool IsFavorite { get; set; }
}
Code Behind:
namespace myApp
{
public sealed partial class MainPage
{
public MainPage()
{
InitializeComponent();
DataContext = new ViewModel;
}
}
}
My "ViewModel"-Class:
namespace myApp
{
public class ViewModel
{
public ObserverableCollection<DisruptionDisplayElement> DisruptionDisplayList {get;set;}
public ViewModel()
{
DisruptionDisplayList = new ObservableCollection<DisruptionDisplayElement>();
DisruptionDisplayList.Add(new DisruptionDisplayElement() { IsFavorite = true, Message = "Message 1"});
DisruptionDisplayList.Add(new DisruptionDisplayElement() { IsFavorite = false, Message = "Message 2" });
DisruptionDisplayList.Add(new DisruptionDisplayElement() { IsFavorite = true, Message = "Message 3" });
}
}
}
What can I do to hide the ListViewItem without wasting all the space for emtpy ListViewItems if I hide the grid inside?
Edit:
Advanced the Code providing
Without a good, minimal, complete code example, it's not possible to provide an actual code example that shows the correct technique in your exact scenario.
However, the basic answer is that you should be using ICollectionView to filter the visual presentation based on some property.
There are a variety of ways to go about doing this. One of the simplest is to apply the filtering to the default view for your data source. Such a view always exists, and if you are only binding your data source to a single visual object, or you want all visual objects that present that data source to be filtered in the same way, then getting and modifying this view is the correct approach.
As an example:
ICollectionView view = CollectionViewSource.GetDefaultView(DisruptionDisplayList);
view.Filter = item => ((MyClass)item).IsFavorite;
You would configure this view in your code-behind at the appropriate time, e.g. when the user indicates through whatever input mechanism you've provided that they want to show only the favorite items.
Note that this approach avoids altogether trying to use the DataTemplate as the mechanism for showing or hiding the items. Instead, the items are filtered before they ever reach the ListView object.
if you are looking for UWP, just set MinHeight Property to 0 (zero):
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="MinHeight" Value="0" />
</Style>
</ListView.ItemContainerStyle>
I have an unknown list of variables that I wish to bind to specific controls within a WPF application. Is there a way to bind a variable out of the list with a specific name?
Here is a code example of what I am trying to do.
C#
public class Variable {
public string Name {get;set;}
}
public class VariableViewModel : INotifyPropertyChanged {
Variable _variable;
public Variable Variable {
get {
return(_variable);
}
set {
_variable = value;
if(PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs("Variable"));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class VariableListViewModel {
public ObservableCollection<VariableViewModel> VariableList { get; set; }
public VariableListViewModel() {
VariableList = new ObservableCollection<VariableViewModel>();
var variableViewModel = new VariableViewModel {
Variable = new Variable { Name = "my_variable_name" }
};
VariableList.Add(variableViewModel);
}
}
WPF:
<Window>
<Window.DataContext>
<local:VariableListViewModel />
</Window.DataContext>
<StackPanel>
<Label Content="{Binding Path=Name, ElementName=my_variable_name}" />
</StackPanel>
</Window>
The label is clearly wrong here. My question is whether or not what I am trying to achieve is possible? I want to be able to display 'my_variable_name'.
-Stuart
You would normally use a ListView (or other ItemsControl), and bind its ItemsSource to your collection (VariableList).
This will cause each item (a VariableViewModel) to get displayed. You'd then use a DataTemplate to cause the VariableViewModel to display as a Label bound to Variable.Name.
You could use a ValueConverter and use its ConverterParameter to pass in the name of the variable:
public object ConvertTo(object value, Type targetType, object parameter, CultureInfo culture)
{
var variableList = (IEnumerable<Variable>)value;
var variableName = parameter != null ? parameter.ToString() : string.Empty;
return variableList.FirstOrDefault(v=>v.Name == variableName);
}
You could then use that converter in XAML as follows:`
<Label Content="{Binding Path=ListOfVariables,
Converter={StaticResource VariableConverter},
ConverterParameter='VariableName'}" />
Your question contradicts itself.
On the one hand you say you have an unknown list of variables, on the other hand your View knows which variables it wants to bind to.
So, either you know which variables you want to bind to or you don't know it.
Scenario 1: You know which variables you want to bind to:
Create a property per Variable in your ViewModel
Scenario 2: You don't know which variables you want to bind to:
Bind your VariableList property to an ItemsControl in the View and provide a DataTemplate that can render a VariableViewModel.
BTW: Having a VariableViewModel seems to be superfluous. You could use Variable directly. Especially when all VariableViewModel does is returning the Variable inside.
Based on Reeds solution and comments, here is my final solution. I used the same model and view model, so I will only show the View.
<Window>
<Window.DataContext>
<local:VariableListViewModel />
</Window.DataContext>
<Window.Resources>
<DataTemplate x:Key="itemsControlDataTemplate">
<Label x:Name="label" Content="{Binding Variable.Name}">
<Label.Style>
<Style TargetType="{x:Type Label}">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<Trigger Property="Content" Value="my_variable_name">
<Setter Property="Visibility" Value="Visible" />
</Trigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
</DataTemplate>
</Window.Resources>
<StackPanel>
<ItemsControl ItemTemplate="{StaticResource itemsControlDataTemplate}" ItemsSource="{Binding Path=VariableList}" />
</StackPanel>
</Window>
I have ContentPresenter with DataTemplateSelector:
...
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var model = item as ItemControlViewModel;
if (model.CurrentStatus == PrerequisitesStatus.Required)
{
return RequiredTemplate;
}
if (model.CurrentStatus == PrerequisitesStatus.Completed)
{
return FinishedTemplate;
}
...
return InProgressTemplate;
}
When CurrentStatus is changed, OnPropertyChanged is called.
I need somehow to trigger this DataTemplateSelector when the property is changed and change ContentPresenter DataTemplate. Any suggestions?
Threre are similar questions:
1
2, but I don't want to use any DataTriggers, because of too much states.
Tried to play with DataTriggers
<ContentPresenter
Grid.Column="1"
Height="16"
Width="16"
Margin="3">
<ContentPresenter.Triggers>
<DataTrigger Binding="{Binding Path=CurrentStatus}" Value="0">
<Setter Property="ContentPresenter.ContentTemplate" Value="{StaticResource ResourceKey=_requiredStatusTemplate}" />
</DataTrigger>
</ContentPresenter.Triggers>
</ContentPresenter>
But got an error:
Triggers collection members must be of type EventTrigger :(
As you requested an example with datatriggers in the comments, here you are:
A FrameworkElement can only have EventTriggers, therefore you get the error Message Triggers collection members must be of type EventTrigger
And also don't use a ContentPresenter directly, it is meant to be used inside a ControlTemplate. Better use a ContentControl when you want to have dynamic content.
See What's the difference between ContentControl and ContentPresenter?
And finally here's a suggestion to your DataTrigger issue. I have put it inside a style for reusability ....
XAML :
<Window x:Class="WpfApplication88.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate x:Key="requiredTemplate">
<TextBlock Text="requiredTemplate"></TextBlock>
<!--your stuff here-->
</DataTemplate>
<DataTemplate x:Key="completedTemplate">
<TextBlock Text="CompletedTemplate"></TextBlock>
<!--your stuff here-->
</DataTemplate>
<Style x:Key="selectableContentStyle" TargetType="{x:Type ContentControl}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=CurrentStatus}" Value="Required">
<Setter Property="ContentTemplate" Value="{StaticResource requiredTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=CurrentStatus}" Value="Completed">
<Setter Property="ContentTemplate" Value="{StaticResource completedTemplate}" />
</DataTrigger>
<!-- your other Status' here -->
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<ContentControl Width="100" Height="100" Style="{StaticResource selectableContentStyle}"/>
</Grid>
</Window>
I could be wrong, but I believe the DataTemplateSelector is only used when the ItemContainerGenerator creates a container for an item added to the collection. Because a new container isn't generated when a property value changes, a new DataTemplate is never going to be applied via the selector.
As suggested in the comments, I would recommend you look at the VisualStateManager or data triggers, otherwise you're going to have to recreate the container for every item when one or more properties change value.
Just as an extra choice - if you want to stick to your templates, just use s binding with converter.
I came up with a behavior that would theoretically do this.
C#:
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
public class UpdateTemplateBehavior : Behavior<ContentPresenter>
{
public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(nameof(Content), typeof(object), typeof(UpdateTemplateBehavior), new FrameworkPropertyMetadata(null, OnContentChanged));
public object Content
{
get => GetValue(ContentProperty);
set => SetValue(ContentProperty, value);
}
static void OnContentChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (sender is UpdateTemplateBehavior behavior)
behavior.Update();
}
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(nameof(Value), typeof(object), typeof(UpdateTemplateBehavior), new FrameworkPropertyMetadata(null, OnValueChanged));
public object Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
static void OnValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (sender is UpdateTemplateBehavior behavior)
behavior.Update();
}
public UpdateTemplateBehavior() : base() { }
protected override void OnAttached()
{
base.OnAttached();
Update();
}
void Update()
{
if (Content != null)
{
BindingOperations.ClearBinding(AssociatedObject, ContentPresenter.ContentProperty);
AssociatedObject.Content = null;
BindingOperations.SetBinding(AssociatedObject, ContentPresenter.ContentProperty, new Binding() { Path = nameof(Content), Source = this });
}
}
}
XAML:
<ContentPresenter ContentTemplateSelector="{StaticResource MySelector}">
<i:Interaction.Behaviors>
<Behavior:UpdateTemplateBehavior Content="{Binding SomeContent}"
Value="{Binding SomeValue}"/>
</i:Interaction.Behaviors>
</ContentPresenter>
The content is "updated" (by clearing and then resetting the binding) when the content (in this example, "SomeContent") and an arbitrary value (in this example, "SomeValue") is changed, as well as when the behavior is first attached.
An update is not made unless the content is not null (my project-specific requirement). Not updating upon attaching may avoid unintentionally updating twice at once, but if the value is initially null, an update wouldn't occur until the value changes at least once.
Note: In the above example, I am not sure if the behavior has the same data context as the ContentPresenter. I use a helper class that I did not include here for brevity. Keep that in mind when testing...
I have a little problem with MVVM. Let me first sketch my problem.
I have a Parent View (DashboardConsultants) which has a datagrid. Each cell in that DataGrid has a tooltip, implemented like this:
<UserControl.Resources>
<ResourceDictionary>
<DataTemplate DataType="{x:Type vm:UC1001_AgreementDetailsViewModel}">
<v:UC1001_AgreementDetails_View />
</DataTemplate>
</ResourceDictionary>
</UserControl.Resources>
<DataGridTextColumn.ElementStyle>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="DataGridCell.ToolTip">
<Setter.Value>
<vm:UC1001_AgreementDetailsViewModel />
</Setter.Value>
</Setter>
The tooltip calls up my ViewModel (AgreementDetailsViewModel), which has the following code:
public UC1001_ActiveAgreementContract AgreementDetailsContract { get; set; }
public int AgreementID { get; set; }
public UC1001_AgreementDetailsViewModel()
{
AgreementDetailsContract = new UC1001_ActiveAgreementContract();
this.Initialize();
}
private void Initialize()
{
GetRefData();
ShowAgreementDetailsView();
}
private void GetRefData()
{
UC1001_ActiveAgreementArguments args = new UC1001_ActiveAgreementArguments();
args.AgreementID = 3;
DefaultCacheProvider defaultCacheProvider = new DefaultCacheProvider();
if (!defaultCacheProvider.IsSet("AgrDet:" + args.AgreementID))
{
ConsultantServiceClient client = new ConsultantServiceClient();
AgreementDetailsContract = client.GetAgreementDetailsByAgreementID(args);
defaultCacheProvider.Set("AgrDet:" + args.AgreementID, AgreementDetailsContract, 5);
}
else
{
AgreementDetailsContract = (UC1001_ActiveAgreementContract)defaultCacheProvider.Get("AgrDet:" + args.AgreementID);
}
}
private void ShowAgreementDetailsView()
{
// Initialize
var regionManager = ServiceLocator.Current.GetInstance<IRegionManager>();
// Show content
var agreementDetailsWorkspace = new Uri("UC1001_AgreementDetails_View", UriKind.Relative);
regionManager.RequestNavigate("ContentRegion", agreementDetailsWorkspace);
}
The goal of the ViewModel is to get an Agreement from the database (currently a static one...) and then pass it on to the child View (UC1001_AgreementDetails_View) to show as the tooltip. The child View has the following constructor so the controls can bind to the contract:
public UC1001_AgreementDetails_View(ViewModels.UC1001_AgreementDetailsViewModel UC1001_AgreementDetailsViewModel)
{
InitializeComponent();
this.DataContext = UC1001_AgreementDetailsViewModel.AgreementDetailsContract;
}
I put a breakpoint on the ViewModel Initialize, and it fires when I get on the parent View, but it should fire when I get on the child View (thus when opening the tooltip in the datagrid). Does anyone know how I can fix this?
More information/code can be provided if needed.
EDIT:
I tried some stuff and I now I got something like this (which I feel is a little closer to the solution).
I changed my tooltip to the following (according to Rachels help):
<Setter Property="DataGridCell.ToolTip">
<Setter.Value>
<v:UC1001_AgreementDetails_View DataContext="{Binding AgreementDetailsViewModel}" />
</Setter.Value>
</Setter>
In my child view, I put the following binding
<Label Content="{Binding AgreementDetailsContract.Header}" Height="50" HorizontalAlignment="Left" Margin="8,6,0,0" Name="_labelHoofding" VerticalAlignment="Top" FontSize="22" />
Now my AgreementDetailsViewModel, which has a property called AgreementDetailsContract with all the information I want to show, is the DataContext of my child view. But my problem still exist. The AgreementDetailsViewModels fires on opening the ConsultantDashboard, when it should open on displaying the tooltip. Is there some kind of event/command I can put on the tooltip to fire the ViewModel? Also, I think something is wrong with the Binding of my label because it doesn't show information (altough it could be the same problem that the ViewModel doesn't pass the right information).
Again, if it looks a little complex to you, I will be happy to explain it further, or give more code if asked.
SOLVED:
I got the solution. I specify the binding in the constructor of the ChildView instead of its XAML or in the View Tooltip.
public UC1001_AgreementDetails_View()
{
InitializeComponent();
this.DataContext = new UC1001_AgreementDetailsViewModel();
}
It looks like your View is directly referencing the ViewModel, which means it will create a copy of your ViewModel when it starts up
This code
<Setter Property="DataGridCell.ToolTip">
<Setter.Value>
<vm:UC1001_AgreementDetailsViewModel />
</Setter.Value>
</Setter>
Should be
<Setter Property="DataGridCell.ToolTip">
<Setter.Value>
<!-- If you want to keep the DataTemplate, use a ContentControl -->
<v:UC1001_AgreementDetails_View DataContext="{Binding AgreementDetails}" />
</Setter.Value>
</Setter>
Your Data Structure should look something like this:
class MainViewModel
{
ObservableCollection<AgreementViewModel> Agreements;
}
class AgreementViewModel
{
// Loaded only when getter is called
AgreementDetailViewModel AgreementDetails;
}
I am having an issue when using a DataTrigger to manipulate the IsEnabled property of a control. Normally it works fine, however when I initialize the IsEnabled state within the View's Initialized event, the dynamic stylizing no longer works.
Here's my code. I trimmed it down to the simplest example I could.
Why is this occurring, and what can I do to allow me to set IsEnabled both by a style trigger and by initializing it in the code behind?
Thanks in advance!
View:
(Contains a textbox that should be enabled/disabled depending on the value of a checkbox)
<Window x:Class="IsEnabled.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Initialized="Window_Initialized">
<StackPanel Orientation="Vertical">
<TextBox x:Name="txtTarget" Width="200">
<TextBox.Style>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=ToggleValue}" Value="True">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
<CheckBox x:Name="chkSource" IsChecked="{Binding Path=ToggleValue}" />
</StackPanel>
</Window>
View Codebehind:
(The only addition is the implementation of the Initialized event setting the inital state for IsEnabled)
using System;
using System.Windows;
namespace IsEnabled.Views
{
public partial class MainView : Window
{
public MainView()
{
InitializeComponent();
}
private void Window_Initialized(object sender, EventArgs e)
{
txtTarget.IsEnabled = false;
}
}
}
ViewModel:
(ViewModelBase holds the implementation of the INotifyPropertyChanged interface)
using System;
namespace IsEnabled.ViewModels
{
class MainViewModel : ViewModelBase
{
private bool _ToggleValue;
public bool ToggleValue
{
get { return _ToggleValue; }
set
{
_ToggleValue = value;
OnPropertyChanged(this, "ToggleValue");
}
}
}
}
Have a look at dependency property value precedence, and how changing values from different places, Styles, Triggers, Animations etc. work together.
Add to your Binding Mode=TwoWay and it should work.