Binding properties to DataTemplate - wpf

I'm using a DataTemplate which is inside a ResourceDictionary file.
<DataTemplate x:Key="AlertWarningMessage">
<Grid>
<Border Visibility="{Binding DataContext.Visibility}" Background="{StaticResource ResourceKey=AlertWarningMessageBackground}" HorizontalAlignment="Stretch" Height="30">
<WrapPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="WARNING !" FontWeight="Bold" Foreground="{StaticResource ResourceKey=AlertWarningMessageForeground}" FontSize="13"/>
<TextBlock Text="{Binding DataContext.Message}" Foreground="{StaticResource ResourceKey=AlertWarningMessageForeground}" Margin="5,0,0,0"/>
</WrapPanel>
</Border>
</Grid>
</DataTemplate>
I merge this dictionnary in my UserControl, and i'm using this template like this :
<ContentControl ContentTemplate="{StaticResource AlertWarningMessage}" Grid.Row="2" Margin="0,2,0,0" DataContext="{Binding AlertSummary, UpdateSourceTrigger=PropertyChanged}" />
In my VM, i'm using a class which have 2 properties :
Public Class AlertInfos
Public Property Visibility As Visibility
Public Property Message As String
Public Sub New(p_visibility As Visibility, p_msg As String)
Me.Visibility = p_visibility
Me.Message = p_msg
End Sub
End Class
Property VM as my class :
Private _alertSummary As AlertInfos
Public Property AlertSummary() As AlertInfos
Get
Return _alertSummary
End Get
Set(ByVal value As AlertInfos)
_alertSummary = value
RaisePropertyChanged("AlertSummary")
End Set
End Property
Properties of this object are set to Collapsed and String.Empty
Next, I change the values of this object, like this :
Public Sub ShowAlert()
Me.AlertSummary.Message = "Test"
Me.AlertSummary.Visibility = Visibility.Visible
'Me.StartTimerAlert()
RaisePropertyChanged("AlertSummary")
End Sub
But it's not working. There are 2 problems :
At the begining, when the Visibility is set to Collapsed, the Border is visible.
When I change the Message property, it is not visually updated.
I think there is a problem with my Binding, but I don't know where. I tried differents things, but there is always these problems.
Furthermore, I had bind the property directly in a TextBlock below the ContentControl, and the Binding working find.
Do you have any idea ?

You should change your data template to this:
<DataTemplate x:Key="AlertWarningMessage">
<Grid>
<Border Visibility="{Binding RelativeSource={RelativeSource AncestorType=ContentControl}, Path=DataContext.Visibility}" Background="AliceBlue" HorizontalAlignment="Stretch" Height="30">
<WrapPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="WARNING !" FontWeight="Bold" Foreground="Red" FontSize="13"/>
<TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType=ContentControl}, Path=DataContext.Message}" Foreground="Red" Margin="5,0,0,0"/>
</WrapPanel>
</Border>
</Grid>
</DataTemplate>
And your AlertInfos to this (it's on C# so try to translate it to VB)
public class AlertInfos
{
private string message;
public string Message
{
get
{
return this.message;
}
set
{
if (this.message != value)
{
this.message = value;
}
}
}
private Visibility visibility;
public Visibility Visibility
{
get
{
return this.visibility;
}
set
{
if (this.visibility != value)
{
this.visibility = value;
}
}
}
}
It should work, at least it's working on my PC

I'm not familiar with VB but Message needs to RaisePropertyChanged
Visibilities are commonly bound too bools which also RaisePropertyChanged - these then use a BooleanToVisibilityConverter
Make sure your properties are public - have private backing variables and RaisePropertyChanged.
private bool _isSomethingVisibile;
public bool IsSomethingVisibile
{
get { return _isSomethingVisibile; }
set
{
_isSomethingVisibile = value;
RaisePropertyChanged();
}
}
You don't need to preface your bindings with "DataContext" that is implied.

Related

How can I DataBind a textbox from the parent window with values from the child with MVVM?

I just took over a project from another programmer who is no longer here. It was created using the MVVM Pattern (using the MVVM Light toolkit). I am new to MVVM and have been trying to learn the basics fast. Currently I am having trouble getting a selected value from a Child Window back to the Parent Window.
From another post on SO I learned that I should use the same ViewModel for both the parent and the child so I think I have the basics right. However I have not been able to get the selected values back to the parent. Below is a sample set of code similar to the production code.
My ViewModel for both pages is here
public class MainViewModel : ViewModelBase
{
private Vendor selectedVendor = null;
List<Vendor> vendors;
public MainViewModel()
{
OpenVendorWindowCommand = new RelayCommand(VendorSelect);
VendorSelectedCommand = new RelayCommand(VendorSelected);
LoadVendors();
}
public ICommand OpenVendorWindowCommand { get; private set; }
public ICommand VendorSelectedCommand { get; private set; }
void VendorSelect()
{
Messenger.Default.Send(new NotificationMessage("SelectVendor"));
}
public Vendor SelectedVendor
{
get { return selectedVendor; }
set
{
if (selectedVendor != value)
{
selectedVendor = value;
RaisePropertyChanged();
}
}
}
void VendorSelected()
{
Console.WriteLine(SelectedVendor.VendorName);
}
public List<Vendor> Vendors
{
get
{
return vendors;
}
set
{
if (vendors != value)
{
vendors = value;
RaisePropertyChanged();
}
}
}
private void LoadVendors()
{
DataTable dt = new DataTable();
dt = Vendor.GetVendors();
Vendors = new List<Vendor>();
foreach (DataRow row in dt.Rows)
{
Vendors.Add(new Vendor()
{
VendorID = Convert.ToInt32(row["VendorID"]),
VendorCode = Convert.ToString(row["VendorCode"]),
VendorName = Convert.ToString(row["VendorName"])
});
}
}
}
I am at the point that the Child Window opens and I am able to select a vendor from a ListBox. After the selection I press a button (VendorSelectedCommand) and it is at that point I want the textbox on the Parent Window to be filled with the SelectedVendor.VendorName value.
This is the XAML from my Child Window
<StackPanel VerticalAlignment="Center">
<ListBox
Height="200"
Margin="5"
HorizontalAlignment="Stretch"
Background="GhostWhite"
ItemsSource="{Binding Vendors}"
SelectedItem="{Binding Path=SelectedVendor, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<Border BorderBrush="Black" BorderThickness="3">
<StackPanel Margin="15">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="175" />
<ColumnDefinition Width="100" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
FontWeight="SemiBold"
Foreground="Black"
Text="{Binding VendorName}" />
<TextBlock
Grid.Column="1"
FontWeight="SemiBold"
Foreground="Black">
<Run Text=" (" />
<Run Text="{Binding VendorCode}" />
<Run Text=") " />
</TextBlock>
</Grid>
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Command="{Binding VendorSelectedCommand}" Content="Send Vendor Back" />
</StackPanel>
And lastly this is the XAML for the Parent Window with what I think is the correct binding
<StackPanel VerticalAlignment="Center">
<TextBox Margin="10" Text="{Binding SelectedVendor.VendorName}" />
<Button
Margin="10"
Command="{Binding OpenVendorWindowCommand}"
Content="Select Vendor" />
</StackPanel>
I have tried every possible combination of Binding Syntax that I can think of and have tried multiple different ways in the code behind to catch and bind it but have not been able to get it right. What is missing from my ViewModel to make this work?
Edit For clarity (and in response to a comment) I am adding the DataContext, which I had in the Constructor of the Views.
public partial class VendorView : Window
{
private MainViewModel _vm = null;
public VendorView()
{
InitializeComponent();
_vm = new MainViewModel();
DataContext = _vm;
}
}
Edit #2 I am opening the second page with this. This is very simple sample app with only two pages so I didn't want to get bogged down with navigation until I have a better handle on Binding.
private void NotificationMessageReceived(NotificationMessage msg)
{
if (msg.Notification == "SelectVendor")
{
var vendorView = new VendorView();
vendorView.ShowDialog();
}
}

View is not binding correctly to ViewModel

I cannot get the View to bind correctly to the ViewModel. When it displays, it only shows the string version of the ViewModel.
I have seen: Setting Window.Content to ViewModel - simple data template not working. But the link is no longer available.
I'm trying to use https://msdn.microsoft.com/en-us/magazine/dd419663.aspx, as a template.
MainWindow.xaml
<Window x:Class="DemoApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:DemoApp.ViewModel"
xmlns:vw="clr-namespace:DemoApp.View">
<Window.Resources>
<DataTemplate DataType="{x:Type vm:TestViewModel}">
<vw:TestView/>
</DataTemplate>
<DataTemplate x:Key="ClosableTabItemTemplate">
<DockPanel Width="120">
<Button
Command="{Binding Path=CloseCommand}"
Content="X"
Cursor="Hand"
DockPanel.Dock="Right"
VerticalContentAlignment="Bottom"
Width="16" Height="16"/>
<ContentPresenter
Content="{Binding Path=DisplayName}"
VerticalAlignment="Center"/>
</DockPanel>
</DataTemplate>
<DataTemplate x:Key="WorkspacesTemplate">
<TabControl
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding}"
ItemTemplate="{StaticResource ClosableTabItemTemplate}"
Margin="4" />
</DataTemplate>
</Window.Resources>
<DockPanel>
<Border
Grid.Column="2"
Style="{StaticResource MainBorderStyle}">
<HeaderedContentControl
Content="{Binding Path=Workspaces}"
ContentTemplate="{StaticResource WorkspacesTemplate}"
Header="Workspaces"
Style="{StaticResource MainHCCStyle}" />
</Border>
</DockPanel>
</Window>
MainWindowViewModel.cs
// ommitted for clarity. This is directing to the view model correctly. It's the binding between View and ViewModel that is not
TestView.xaml
public class TestViewModel : WorkspaceViewModel, INotifyPropertyChanged,
{
public Model.Test _test;
public string DisplayName {get; set;}
public class TestViewModel(Model.Test t)
{
DisplayName = "Test Display Name";
_model = t;
}
// INofifyPropertyChanged Members removed for clarity
}
Test.cs
public class Test
{
public string FirstName {get; set;}
public string LastName {get; set;}
public static DisplayTest()
{
return new Test();
}
}
Displays:
DemoApp.ViewModel.TestViewModel;
However, when I go to the MainWindow.xaml and actually type in into a DockPanel, it will display correctly...
Thank you!!
UPDATE:
MainWindowViewModel.cs Properties
public ReadOnlyCollection<CommandViewModel> Commands
{
get
{
if (_commands == null)
{
List<CommandViewModel> cmds = this.CreateCommands();
_commands = new ReadOnlyCollection<CommandViewModel>(cmds);
}
return _commands;
}
}
public ObservableCollection<WorkspaceViewModel> Workspaces
{
get
{
if (_workspaces == null)
{
_workspaces = new ObservableCollection<WorkspaceViewModel>();
_workspaces.CollectionChanged += this.OnWorkspacesChanged;
}
return _workspaces;
}
}
In the View there was a Data Context Declared. This was confusing the binding it looks like. Once the Data Context in the View was removed and the MainWindowResourses kept the data context, the view is displayed as it should.

WPF Textbox TwoWay binding in datatemplate not updating the source even on LostFocus

I have an ObservableCollection<string> Tags as part of a custom object. I bind it to a DataTemplate in order to show all tags to the user with the following code:
<StackPanel DockPanel.Dock="Top" Margin="15,0,15,0" Orientation="Horizontal">
<Label Content="Tags:" FontSize="14" Foreground="{StaticResource HM2LightTextBrush}"/>
<Grid>
<ItemsControl Name="PanelPreviewNoteTags" ItemsSource="{Binding ElementName=lbNotesQuickView, Path=SelectedItem.Tags}" Margin="3,0" Visibility="Collapsed">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderThickness="1" BorderBrush="#676B6E" Margin="3,0">
<Label Content="{Binding .,Mode=OneWay}" Foreground="{StaticResource HM2LightTextBrush}"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl Name="PanelEditNoteTags" ItemsSource="{Binding ElementName=lbNotesQuickView, Path=SelectedItem.Tags}" Margin="3,0" Visibility="Collapsed">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderThickness="1" BorderBrush="#676B6E" Margin="3,0">
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding ., Mode=TwoWay}"/>
<Button Style="{StaticResource RibbonButton}" Click="ButtonRemoveTagClick" Tag="{Binding}">
<Image Height="16" Width="16" Source="/Poker Assistant;component/Resources/fileclose.png" />
</Button>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</StackPanel>
Adding and removing items from the ObservableCollection works as expected.
In code I switch between edit and view mode by setting the Visibility of the corresponding PanelEditNoteTags and PanelPreviewNoteTags. This all good and working. But when I enter the edit mode and start typing new values for the tags in the TextBox the source doesn't get updated. I certainly know that the LostFocus event is raised when I press my Save button. I tried all UpdateSourceTrigger values, still the same.
Is it a problem related to two controls binding at the same time to the same value - the Label from PanelPreviewNoteTags and the TextBox from PanelEditNoteTags?
What am I missing here?
#Clemens Thank you for the quick and accurate response :) Following is the working solution for future reference.
The solution is not to use ObservableCollection<string> Tags because as pointed by Clemens the {Binding ., Mode=TwoWay} does not work back to the source.
So I created a custom Tag class:
public class Tag : INotifyPropertyChanged
{
private string _content;
public string Content { get { return _content; } set { _content = value; OnMyPropertyChanged(() => Content); } }
public Tag(string content)
{ Content = content; }
public Tag()
: this("new tag")
{ }
public event PropertyChangedEventHandler PropertyChanged;
// Raise the event that a property has changed in order to update the visual elements bound to it
internal void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
//CONVERTS the passed parameter to its name in string
internal void OnMyPropertyChanged<T>(Expression<Func<T>> memberExpression)
{
MemberExpression expressionBody = (MemberExpression)memberExpression.Body;
OnPropertyChanged(expressionBody.Member.Name);
}
public override string ToString()
{
return Content;
}
}
And use it as ObservableCollection<Tag> Tags. Then bind to it like so
<TextBox Text="{Binding Content, Mode=TwoWay}" Tag="{Binding}"/>
I actually populate from and save to postgres database in a string array column, so I need to convert to and from string[]. These are my conversions:
string[] array = note.Tags.Select(item => item.Content).ToArray();
note.Tags = new ObservableCollection<Tag>((array.Select(item => new Tag() { Content = item }).ToList()));

How to put an Expander inside Datatemplate?

Strange one.
I have a contentcontrol on a WPF form, this loads a datatemplate within it.
This shows up fine (handwritten summary code so ignore errors/lack of attributes):
<DataTemplate>
<Label Content="Found datatemplate" />
</DataTemplate>
This however renders blank
<DataTemplate>
<Expander Header="Why dont I show">
<Label Content="Found datatemplate" />
</Expander>
</DataTemplate>
I have set the expander to visibile, isexpanded to true etc and no matter what it doesn't render at all.
Confused- is this just not possible?
I've recently done something similar to what you're describing and it worked for me. I have an ItemsControl that binds to a collection of view models, each of which contains a UserControl representing custom content. I implemented the ItemsControl.ItemTemplate to display the custom control inside an Expander like this:
<ItemsControl Margin="0,20,0,0" ItemsSource="{Binding ControlItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Margin="0,0,0,0"
BorderBrush="#E7E7E7"
BorderThickness="0,1,0,0"
Padding="20,0">
<Expander Foreground="#E7E7E7"
IsExpanded="{Binding Path=IsExpanded,
Mode=TwoWay}">
<Expander.Header>
<Grid>
<TextBlock HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="24"
Text="{Binding Title}" />
</Grid>
</Expander.Header>
<DockPanel>
<ScrollViewer MinHeight="250">
<ContentControl Content="{Binding Control}" />
</ScrollViewer>
</DockPanel>
</Expander>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
This is what my view model looks like:
public class SidePanelControlItem : ModelBase
{
private bool _isExpanded;
public SidePanelControlItem(UserControl control)
{
if (control == null) { throw new ArgumentNullException("control");}
Control = control;
}
public string Title { get; set; }
public UserControl Control { get; private set; }
public bool IsExpanded
{
get { return _isExpanded; }
set
{
_isExpanded = value;
OnPropertyChanged("IsExpanded");
}
}
}

Handling GridItems click

Starting with a Grouped Items Page template, I want to be able to perform tasks on the grid items when they are clicked. Namely, I want to change the background image, and add/remove the underlying object to a list of selected items.
Here's my DataTemplate:
<GridView.ItemTemplate>
<DataTemplate>
<Border BorderBrush="LightGray" BorderThickness="2" Margin="0,0,20,20">
<Grid HorizontalAlignment="Left" Width="390" Height="190">
<Grid.Background>
<ImageBrush ImageSource="/Assets/unselected.png" Stretch="None"/>
</Grid.Background>
<StackPanel Orientation="Horizontal" VerticalAlignment="Top">
<Image VerticalAlignment="Top" Stretch="None" Source="{Binding ImageUrl}" Margin="10,10,0,0"/>
<StackPanel MaxWidth="270">
<TextBlock Text="{Binding Summary}"/>
<TextBlock Text="{Binding Brand}" />
<TextBlock Text="{Binding Detail}" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</GridView.ItemTemplate>
OnTap, I want to togle the ImageSource value of the Grid.Background from unselected.png to selected.png. This I believe I can do using VisualStates and Storyboards, but I've been unable to get this to work in the past (I'll spare you the chaos of my attempts in xaml).
Needless to say, I've tried following the steps detailed here using Blend, but the Grid.Background property doesn't seems to be state specific. If I try changing the background brush in the Pressed or Selected states, it also changes for the Normal state.
Since I want to grab the data context of the selected item and add/remove it from a list, should I just be handling all this together in an OnTap event handler? I would prefer to keep these concerns separated, but I'll do what I need to...
thanks!
One clean way to do this would be engage the selection method (Tap) in such a way that it only opperates on its items, and the items themselves have properties which Implement the INotifyPropertyChanged interface
Your View Model would have a collection of your custom objects that have properties that can notify the ui
public class MyObject : INotifyPropertyChanged
{
private string _summary;
public string summary
{
get {return _summary}
set
{
_summary = value;
OnPropertyChanged()
}
}
//Other Properties: brand || detail
private ImageSource _backgroundImage;
public ImageSource backgroundImage
{
get {return _backgroundImage}
set
{
_backgroundImage = value;
OnPropertyChanged()
}
}
private bool _IsSelected;
public bool IsSelected
{
get {return _IsSelected;}
set
{
_IsSelected = value;
OnPropertyChanged()
}
}
}
Then although your code behind can be used to change the value of IsSelected, or Background image ... if you choose to go with IsSelected, you can still separate your concerns by not directly setting the resource of the background image in code behind. The Codebehind will only iterate over the items to toggle the IsSelected property, and you can use xaml to define the image that the background should use by creating a custom converter.
public class MyCustomControlOrPage.cs : UserControl //Or ApplicationPage
{
//.......code
protected void HandleTap(object sender, GestureEventArgs e)
{
foreach(var item in ((Listbox)sender).ItemsSource)
{
((MyObject)item.IsSelected = (MyObject)item.Name == (e.NewItems[0] as MyObject).Name? true: false;
}
}
}
then the converter
public class BackgroundConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
ImageSource source = value == true ? new BitmapImage(uriForSelected) : new BitmapImage(uriForunselected);
return source;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
BitmapImage thisValue = value as BitmapImage;
return thisValue;
}
}
and FINALLY the XAML where the grid background binds to the IsSelected property and allows the converter to transform the bool to an ImageSource of type BitmapImage:
//add xmlns:Converters=clr-namesapce:Yournamespace.UpTo.TheNamespaceBackgroundConverterIsIn" to the page or control definition
<UserControl.Resources>
<Converters:BackgroundConverter x:key="BgSourceConverter" />
</UserControl.Resources>
<GridView.ItemTemplate>
<DataTemplate>
<Border BorderBrush="LightGray" BorderThickness="2" Margin="0,0,20,20">
<Grid HorizontalAlignment="Left" Width="390" Height="190">
<Grid.Background>
<ImageBrush ImageSource="{Binding Path=IsSelected, Mode=TwoWay, Converter={Binding Source={StaticResource BGSourceConverter}}}" Stretch="None"/>
</Grid.Background>
<StackPanel Orientation="Horizontal" VerticalAlignment="Top">
<Image VerticalAlignment="Top" Stretch="None" Source="{Binding ImageUrl}" Margin="10,10,0,0"/>
<StackPanel MaxWidth="270">
<TextBlock Text="{Binding Summary}"/>
<TextBlock Text="{Binding Brand}" />
<TextBlock Text="{Binding Detail}" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</GridView.ItemTemplate>

Resources