I'm working on a WPF MVVM application. I'm looking to databind a WebBrowser control to a view model which is in turn bound to a Tab. Following the advice in this article, I created a static helper class consisting of a static DependancyProperty:
public static class WebBrowserHelper
{
public static readonly DependencyProperty BodyProperty =
DependencyProperty.RegisterAttached("Body", typeof(string), typeof(WebBrowserHelper), new PropertyMetadata(OnBodyChanged));
public static string GetBody(DependencyObject dependencyObject)
{
return (string)dependencyObject.GetValue(BodyProperty);
}
public static void SetBody(DependencyObject dependencyObject, string body)
{
dependencyObject.SetValue(BodyProperty, body);
}
private static void OnBodyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
string newValue = (string)e.NewValue;
var webBrowser = (WebBrowser)d;
webBrowser.NavigateToString(newValue);
}
}
XAML Binding WebBrowser to DependancyProperty:
<WebBrowser Grid.Column="2" HorizontalAlignment="Center" src:WebBrowserHelper.Body="{Binding HTMLBody}" VerticalAlignment="Center" Height="Auto" Width="Auto" />
ViewModel that bound to ItemsSource of Tab Control:
public class SomeVM : ViewModelBase, INotifyPropertyChanged
{
private string _htmlBody;
private SomeView _myView = new SomeView();
public SomeVM (string tabName)
{
TabName = tabName;
string contentsAsHTML = do_a_whole_bunch_of_stuff_to_generate_an_HTML_string();
HTMLBody = contentsAsHTML;
}
public string HTMLBody
{
get { return _htmlBody; }
set
{
if (_htmlBody != value)
{
_htmlBody = value;
RaisePropertyChanged("HTMLBody");
}
}
}
public SomeView View
{
get {return _myView;}
set { }
}
public string TabName { get; set; }
}
MainViewModel, Creating the Tab collection:
private ObservableCollection<SomeVM> _tabs;
public ObservableCollection<SomeVM> Tabs
{
get
{
if (_tabs== null)
{
_tabs= new ObservableCollection<SomeVM>();
_tabs.Add(new SomeVM("Tab 1"));
_tabs.Add(new SomeVM("Tab 2"));
_tabs.Add(new SomeVM("Tab 3"));
}
return _tabs;
}
}
MainWindow.xaml setting up the Tab Binding:
<TabControl ItemsSource="{Binding Tabs, Source={StaticResource vm}}"
>
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock
Text="{Binding TabName}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding View}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
My problem is that "OnBodyChanged" is fired multiple times on ever tab change. The HTML takes a few seconds to load, and I would rather it only loads when the property is actually modified in the viewmodel.
EDIT
Here's the smallest sample project that recreates my problem.
Your problem is not relevant to attached properties or MVVM.
In fact, the real problem is that TabControl destroy and recreate its child every time you change the selected tab. That would explain why the handler is invoked more than once. The VisualTree only contains the selected Tab.
If you can try with another control, you will see there are no errors.
For solving this issue, I will redirect you to this post.
Related
I'm trying to create a UserControl in my WPF project which I want should have a DependencyProperty that I can bind to in the parent. The project is written as MVVM and I'm using Caliburn micro.
I really want to write clean and maintainable code using MVVM, so I want my UserControls to utilize viewmodels as much as possible and code behind as little as possible.
The problem is that I'm unsuccessful in getting the binding between the parent and the UserControl viewmodel to work correctly.
MyUserControl:
public partial class MyUserControlView : UserControl
{
public MyUserControlView()
{
InitializeComponent();
// If no Datacontext is set, binding between parent property and textbox text works - one way only (set from parent)!.
// -
// If Datacontext is set to this, bindings with properties in MyUserControlView code behind works.
//DataContext = this;
// If Datacontext is set to MyUserControlViewModel, binding between MyUserControlViewModel and MyUserControlView works, but not with parent.
DataContext = new MyUserControlViewModel();
}
public string ProjectNumber
{
get { return (string)GetValue(MyUserControlValueProperty); }
set { SetValue(MyUserControlValueProperty, value); }
}
public static readonly DependencyProperty MyUserControlValueProperty =
DependencyProperty.Register("ProjectNumber", typeof(string), typeof(MyUserControlView), new PropertyMetadata(null, new PropertyChangedCallback(OnProjectNumberUpdate)));
private static void OnProjectNumberUpdate(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var view = d as MyUserControlView;
view.ProjectNumberText.Text = e.NewValue as string;
}
}
MyUserControl code behind:
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="In MyUserControl: " />
<TextBlock Text="{Binding ProjectNumber}" />
</StackPanel>
<TextBox Name="ProjectNumberText" Text="{Binding ProjectNumber, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" />
</StackPanel>
MyUserControl ViewModel:
public class MyUserControlViewModel : Screen
{
private string _projectNumber;
public string ProjectNumber
{
get { return _projectNumber; }
set
{
_projectNumber = value;
NotifyOfPropertyChange(() => ProjectNumber);
}
}
}
Parent view:
<StackPanel>
<local:MyUserControlView ProjectNumber="{Binding ParentProjectNumber}" />
<StackPanel Orientation="Horizontal">
<TextBlock Text="In parent: "/>
<TextBlock Text="{Binding ParentProjectNumber}" />
</StackPanel>
</StackPanel>
Parent ViewModel:
public class ShellViewModel : Screen
{
public ShellViewModel()
{
ParentProjectNumber = "Hello from parent!";
}
private string _parentProjectNumber;
public string ParentProjectNumber
{
get { return _parentProjectNumber; }
set
{
_parentProjectNumber = value;
NotifyOfPropertyChange(() => ParentProjectNumber);
}
}
}
I know I'm probably way off here, but I have no idea what to do to get the bindings to work correctly.
Is there a better way to bind between a DependencyProperty and a viewmodel? Can I put the DP in the viewmodel somehow?
Here is the entire project solution: https://github.com/ottosson/DependencyPropertyTest
don't change UserControl.DataContext from inside UserControl. it can and will create issues later.
use proper name for DP (ProjectNumberProperty and corresponding ProjectNumber) and add BindsTwoWayByDefault to metadata:
public partial class MyUserControlView : UserControl
{
public MyUserControlView()
{
InitializeComponent();
}
public string ProjectNumber
{
get { return (string)GetValue(ProjectNumberProperty); }
set { SetValue(ProjectNumberProperty, value); }
}
public static readonly DependencyProperty ProjectNumberProperty = DependencyProperty.Register
(
"ProjectNumber",
typeof(string),
typeof(MyUserControlView),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)
);
}
fix bindings in xaml:
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="In MyUserControl: " />
<TextBlock Text="{Binding Path=ProjectNumber, RelativeSource={RelativeSource AncestorType=UserControl}}" />
</StackPanel>
<TextBox Text="{Binding Path=ProjectNumber, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, RelativeSource={RelativeSource AncestorType=UserControl}}" />
</StackPanel>
that should do it.
btw, "clean and maintainable code using MVVM" and "want my UserControls to utilize viewmodels as much as possible" sort of contradict each other.
also nothing wrong with code-behind in UserControls as long as that code handles only view functionality. for example: DataGrid source code contains 8000+ LoC
I'm new to Prism, but I have successfully built several WPF/Mvvm-Light applications. I'm using ViewModel-first instaciation for each View/ViewModel pair. The views are all loaded and deactivated when the application opens. Views are activated as a result of catching an aggregate event aimed at them. This is the first view I've tried to bind to data in a ViewModel. The view displays as expected, except that my listbox is never populated. Only the outline of the listbox is visible. If I change the background color of the listbox, the color of the empty listbox is changed. The ViewModel property has eight rows but none of them are visible. I am able to display hardcoded items in the list box. I know that the view model is loading into the view as the data context, since another textblock is able to bind to a ViewModel property It must be something broken in my listbox xaml. Here is some xaml to review:
<UserControl
x:Class="DxStudioSelect.View.DxStudioFindView"
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"
mc:Ignorable="d"
>
<UserControl.Resources>
<DataTemplate x:Key="DxStudioListTemplate">
<TextBlock Text="{Binding Path=FriendlyForkName}"/>
</DataTemplate>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ListBox
Grid.Column="0"
ItemsSource="{Binding DatabaseInstanceList}"
ItemTemplate="{StaticResource DxStudioListTemplate}"
/>
<TextBlock Text="{Binding Path=PageName}" Grid.Column="1" FontSize="32" Foreground="Green" TextAlignment="Right"/>
</Grid>
</UserControl>
Here is the code-behind:
public partial class DxStudioFindView : UserControl, IDxStudioFindView {
public DxStudioFindView() {
InitializeComponent();
}
public IViewModel ViewModel {
get { return (IDxStudioFindViewModel)DataContext; }
set { DataContext = value; }
}
}
Here is the ViewModel:
private readonly IEventAggregator _eventAggregator;
private readonly IUnityContainer _unityContainer;
private readonly IRegionManager _regionManager;
private readonly string _dxStudioDatabaseName;
private readonly HeaderUpdatePayload _headerUpdatePayload = new HeaderUpdatePayload("DxStudio", "Select DxStudio Instance");
public DxStudioFindViewModel(IUnityContainer unityContainer, IRegionManager regionManager, IEventAggregator eventAggregator, IDxStudioFindView view)
: base(view) {
_unityContainer = unityContainer;
_regionManager = regionManager;
_eventAggregator = eventAggregator;
View.ViewModel = this;
if(IsInDesignMode) {
//Design-time, so show fake data
DesignTimeDataLoad();
} else {
//Run-time, so do the real stuff
DesignTimeDataLoad();
_dxStudioDatabaseName = LiteralString.DxStudioDatabaseNameTest;
_eventAggregator.GetEvent<ViewChangeRequestEvent>().Subscribe(DxStudioInstanceChangeRequest, ThreadOption.UIThread, false, target => target.TargetView == LiteralString.DxStudioFind);
}
}
public string PageName { get; set; }
//public string PageName { get { return "Find DxStudio Instance"; } }
private ObservableCollection<IDxStudioInstanceDto> _dxStudioInstanceList = null;
public ObservableCollection<IDxStudioInstanceDto> DxStudioInstanceList {
get { return _dxStudioInstanceList; }
set {
_dxStudioInstanceList = value;
OnPropertyChanged("DxStudioInstanceList");
}
}
private void DxStudioInstanceChangeRequest(ViewChangeRequestPayload payload) {
var region = _regionManager.Regions[RegionNames.Content];
region.Activate(View);
_eventAggregator.GetEvent<ViewChangedHeaderEvent>().Publish(_headerUpdatePayload);
var footerUpdatePayload = new FooterUpdatePayload(FooterDisplayMode.DxStudioSelect, _dxStudioDatabaseName, payload.TargetBackDatabase, payload.TargetBack, string.Empty, LiteralString.ToolboxStart);
_eventAggregator.GetEvent<ViewChangedFooterEvent>().Publish(footerUpdatePayload);
}
private void DesignTimeDataLoad() {
PageName = "Find DxStudio Instance";
DxStudioInstanceList = new ObservableCollection<IDxStudioInstanceDto>() {
new DxStudioInstanceDto("Instance1"),
new DxStudioInstanceDto("Instance2"),
new DxStudioInstanceDto("Instance3"),
new DxStudioInstanceDto("Instance4"),
new DxStudioInstanceDto("Instance5"),
new DxStudioInstanceDto("Instance6"),
new DxStudioInstanceDto("Instance7"),
new DxStudioInstanceDto("Instance8"),
};
}
And here is the data transfer object:
public class DxStudioInstanceDto : IDxStudioInstanceDto {
public string FriendlyForkName { get; private set; }
public DxStudioInstanceDto(string friendlyForkName) { FriendlyForkName = friendlyForkName; }
}
Since I'm completely out of ideas, any suggestion would be helpful.
Thanks
Your list is binding to ItemsSource="{Binding DatabaseInstanceList}" but your view model has the property DxStudioInstanceList.
I have an ObservableCollection of "Layouts" and a "SelectedLocation" DependencyProperty on a Window. The SelectedLocation has a property called "Layout", which is an object containing fields like "Name" etc. I'm trying to bind a combobox to the SelectedLayout but it's not working.
The following does not work, I've tried binding to SelectedItem instead to no avail. I believe it may be something to do with the fact that I'm binding to a subProperty of the SelectedLocation DependencyProperty (though this does implement INotifyPropertyChanged.
<ComboBox Grid.Row="2" Grid.Column="0" x:Name="cboLayout" ItemsSource="{Binding Layouts,ElementName=root}" SelectedValue="{Binding SelectedLocation.Layout.LayoutID,ElementName=root}" DisplayMemberPath="{Binding Name}" SelectedValuePath="LayoutID" />
However, the following works (Also bound to the "SelectedLocation" DP:
<TextBox Grid.Row="4" Grid.Column="1" x:Name="txtName" Text="{Binding SelectedLocation.Name,ElementName=root,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" />
What type property Layouts has? I suppose something like this this: IEnumerable<Layout>.
But you bind selected value to Layout.LayoutID. So you got situation, when combo box contains Layout objects, and you try to select it by Int identifier. Of course binding engine can't find any Int there.
I have no idea about details of your code, so one thing I could propose: try to reduce your binding expression: SelectedItem="{Binding SelectedLocation.Layout,ElementName=root}.
If no success, provide more code to help me understand what's going on.
====UPDATE====
As I've said, you are obviously doing something wrong. But I am not paranormalist and couldn't guess the reason of your fail (without your code). If you don't want to share your code, I decided to provide simple example in order to demonstrate that everything works. Have a look at code shown below and tell me what is different in your application.
Class Layout which exposes property LayoutId:
public class Layout
{
public Layout(string id)
{
this.LayoutId = id;
}
public string LayoutId
{
get;
private set;
}
public override string ToString()
{
return string.Format("layout #{0}", this.LayoutId);
}
}
Class SelectionLocation which has nested property Layout:
public class SelectedLocation : INotifyPropertyChanged
{
private Layout _layout;
public Layout Layout
{
get
{
return this._layout;
}
set
{
this._layout = value;
this.OnPropertyChanged("Layout");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
var safeEvent = this.PropertyChanged;
if (safeEvent != null)
{
safeEvent(this, new PropertyChangedEventArgs(name));
}
}
}
And Window class with dependency properties (actually, in my example StartupView is UserControl, but it doesn't matter):
public partial class StartupView : UserControl
{
public StartupView()
{
InitializeComponent();
this.Layouts = new Layout[] { new Layout("AAA"), new Layout("BBB"), new Layout("CCC") };
this.SelectedLocation = new SelectedLocation();
this.SelectedLocation.Layout = this.Layouts.ElementAt(1);
}
public IEnumerable<Layout> Layouts
{
get
{
return (IEnumerable<Layout>)this.GetValue(StartupView.LayoutsProperty);
}
set
{
this.SetValue(StartupView.LayoutsProperty, value);
}
}
public static readonly DependencyProperty LayoutsProperty =
DependencyProperty.Register("Layouts",
typeof(IEnumerable<Layout>),
typeof(StartupView),
new FrameworkPropertyMetadata(null));
public SelectedLocation SelectedLocation
{
get
{
return (SelectedLocation)this.GetValue(StartupView.SelectedLocationProperty);
}
set
{
this.SetValue(StartupView.SelectedLocationProperty, value);
}
}
public static readonly DependencyProperty SelectedLocationProperty =
DependencyProperty.Register("SelectedLocation",
typeof(SelectedLocation),
typeof(StartupView),
new FrameworkPropertyMetadata(null));
}
XAML of StartupView:
<UserControl x:Class="Test.StartupView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:self="clr-namespace:HandyCopy"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Name="Root">
<WrapPanel>
<ComboBox ItemsSource="{Binding Path=Layouts,ElementName=Root}"
SelectedItem="{Binding Path=SelectedLocation.Layout, ElementName=Root}"/>
</WrapPanel>
</UserControl>
My question: How do I bind the SelectedItem from a primary datagrid to the ItemsSource for a secondary datagrid?
In detail:
I have two datagrids on my view. The first shows a collection of teams and the second shows as list of people in the selected team.
When I select a team from the grid I can see that the SelectedTeam property is getting updated correctly, but the People grid is not getting populated.
Note: I am not able to use nested grids, or the cool master-detail features provided in the SL data-grid.
UPDATE: Replacing the parent datagrid with a ComboBox gives completely different results and works perfectly. Why would ComboBox.SelectedItem and DataGrid.SelectedItem behave so differently?
Thanks,
Mark
Simple Repro:
VIEW:
<UserControl x:Class="NestedDataGrid.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data">
<StackPanel x:Name="LayoutRoot">
<TextBlock Text="Teams:" />
<data:DataGrid ItemsSource="{Binding Teams}"
SelectedItem="{Binding SelectedTeam, Mode=TwoWay}"
AutoGenerateColumns="False">
<data:DataGrid.Columns>
<data:DataGridTextColumn Header="Id" Binding="{Binding TeamId}" />
<data:DataGridTextColumn Header="Desc" Binding="{Binding TeamDesc}" />
</data:DataGrid.Columns>
</data:DataGrid>
<TextBlock Text="Peeps:" />
<data:DataGrid ItemsSource="{Binding SelectedTeam.People}"
AutoGenerateColumns="False">
<data:DataGrid.Columns>
<data:DataGridTextColumn Header="Id"
Binding="{Binding PersonId}" />
<data:DataGridTextColumn Header="Name"
Binding="{Binding Name}" />
</data:DataGrid.Columns>
</data:DataGrid>
</StackPanel>
</UserControl>
CODE_BEHIND:
using System.Windows.Controls;
namespace NestedDataGrid
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
this.LayoutRoot.DataContext = new ViewModel();
}
}
}
VIEWMODEL:
using System.Collections.ObjectModel;
namespace NestedDataGrid
{
public class ViewModel: ObjectBase
{
public ViewModel()
{
ObservableCollection<Person> RainbowPeeps = new ObservableCollection<Person>()
{
new Person(){ PersonId=1, Name="George"},
new Person(){ PersonId=2, Name="Zippy"},
new Person(){ PersonId=3, Name="Bungle"},
};
ObservableCollection<Person> Simpsons = new ObservableCollection<Person>()
{
new Person(){ PersonId=4, Name="Moe"},
new Person(){ PersonId=5, Name="Barney"},
new Person(){ PersonId=6, Name="Selma"},
};
ObservableCollection<Person> FamilyGuyKids = new ObservableCollection<Person>()
{
new Person(){ PersonId=7, Name="Stewie"},
new Person(){ PersonId=8, Name="Meg"},
new Person(){ PersonId=9, Name="Chris"},
};
Teams = new ObservableCollection<Team>()
{
new Team(){ TeamId=1, TeamDesc="Rainbow", People=RainbowPeeps},
new Team(){ TeamId=2, TeamDesc="Simpsons", People=Simpsons},
new Team(){ TeamId=3, TeamDesc="Family Guys", People=FamilyGuyKids },
};
}
private ObservableCollection<Team> _teams;
public ObservableCollection<Team> Teams
{
get { return _teams; }
set
{
SetValue(ref _teams, value, "Teams");
}
}
private Team _selectedTeam;
public Team SelectedTeam
{
get { return _selectedTeam; }
set
{
SetValue(ref _selectedTeam, value, "SelectedTeam");
}
}
}
}
ASSOCIATED CLASSES:
using System;
using System.ComponentModel;
namespace NestedDataGrid
{
public abstract class ObjectBase : Object, INotifyPropertyChanged
{
public ObjectBase()
{ }
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void _OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler pceh = PropertyChanged;
if (pceh != null)
{
pceh(this, new PropertyChangedEventArgs(propertyName));
}
}
protected virtual bool SetValue<T>(ref T target, T value, string propertyName)
{
if (Object.Equals(target, value))
{
return false;
}
target = value;
_OnPropertyChanged(propertyName);
return true;
}
}
public class Person: ObjectBase
{
private int _personId;
public int PersonId
{
get { return _personId; }
set
{
SetValue(ref _personId, value, "PersonId");
}
}
private string _name;
public string Name
{
get { return _name; }
set
{
SetValue(ref _name, value, "Name");
}
}
}
public class Team : ObjectBase
{
private int _teamId;
public int TeamId
{
get { return _teamId; }
set
{
SetValue(ref _teamId, value, "TeamId");
}
}
private string _teamDesc;
public string TeamDesc
{
get { return _teamDesc; }
set
{
SetValue(ref _teamDesc, value, "TeamDesc");
}
}
private ObservableCollection<Person> _people;
public ObservableCollection<Person> People
{
get { return _people; }
set
{
SetValue(ref _people, value, "People");
}
}
}
}
UPDATE
Replacing the first datagrid with a combobox and eveything works OK. Why would DataGrid.SelectedItem and ComboBox.SelectedItem behave so differently?
<StackPanel x:Name="LayoutRoot">
<TextBlock Text="Teams:" />
<ComboBox SelectedItem="{Binding SelectedTeam, Mode=TwoWay}"
ItemsSource="{Binding Teams}"/>
<TextBlock Text="{Binding SelectedTeam}" />
<TextBlock Text="Peeps:" />
<data:DataGrid ItemsSource="{Binding SelectedTeam.People}" />
</StackPanel>
Having done some tests.
First I just wanted to confirm that the Binding itself is working. It works quite happly when the second DataGrid is swapped out for a ListBox. I've gone so far to confirm that the second DataGrid is having its ItemsSource property changed by the binding engine.
I've also swapped out the first DataGrid for a ListBox and then the second DataGrid starts working quite happly.
In addition if you wire up the SelectionChanged event on the first datagrid and use code to assign directly to the second datagrid it starts working.
I've also removed the SelectedItem binding on the first Grid and set up an ElementToElement bind to it from the on the ItemsSource property of the second Grid. Still no joy.
Hence the problem is narrowed down to SelectedItem on one DatGrid to the ItemsSource of another via the framework binding engine.
Reflector provides a possible clue. The Data namespace contains an Extensions static class targeting DependencyObject which has an AreHandlersSuspended method backed bye a static variable. The which the code handling changes to the ItemsSource property uses this method and does nothing if it returns true.
My unconfirmed suspicion is that in the process of the first Grid assigning its SelectedItem property it has turned on the flag in order to avoid an infinite loop. However since this flag is effectively global any other legitmate code running as a result of this SelectedItem assignment is not being executed.
Anyone got SL4 and fancy testing on that?
Any MSFTers lurking want to look into?
If SL4 still has it this will need reporting to Connect as a bug.
A better solution is to use add DataGridRowSelected command. This fits the MVVM pattern a whole lot better than my previous mouse click example.
This was inspired by some code from John Papa, I have created a detailed post about this http://thoughtjelly.blogspot.com/2009/12/binding-selecteditem-to-itemssource.html.
[Sits back contented and lights a cigar]
Mark
I had the same problem, and "fixed" it by adding this to my code-behind.
Code behind:
private void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_model != null)
{
_model.RefreshDetail();
}
}
Model:
public void RefreshDetail()
{
RaisePropertyChanged("Detail");
}
I have a work-around. It involves a bit of code behind, so won't be favoured by purist MVVM zealots! ;-)
<StackPanel x:Name="LayoutRoot">
<TextBlock Text="Teams:" />
<data:DataGrid x:Name="dgTeams"
SelectedItem="{Binding SelectedTeam, Mode=TwoWay}"
ItemsSource="{Binding Teams}" />
<TextBlock Text="{Binding SelectedTeam}" />
<TextBlock Text="Peeps:" />
<data:DataGrid x:Name="dgPeeps" />
</StackPanel>
Code Behind:
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
this.LayoutRoot.DataContext = new ViewModel();
dgTeams.MouseLeftButtonUp += new MouseButtonEventHandler(dgTeams_MouseLeftButtonUp)
}
void dgTeams_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
DataGridRow row = DependencyObjectHelper.FindParentOfType<DataGridRow>(e.OriginalSource as DependencyObject);
///get the data object of the row
if (row != null && row.DataContext is Team)
{
dgPeeps.ItemsSource = (row.DataContext as Team).People;
}
}
}
The FindParentOfType method is detailed here: http://thoughtjelly.blogspot.com/2009/09/walking-xaml-visualtree-to-find-parent.html.
Hope this helps someone else.
I have one main outstanding issue, I know now how to databind to lists and individual items, but there is one more problem I have with this, I need to bind a listbox to some properties that are in a Class.
For Example I have two Properties that are bound in some XAML in a class called Display:
Public Property ShowEventStart() As Visibility
Public Property ShowEventEnd() As Visibility
I can use these in my XAML but I want them to be in a ListBox/Drop-down List, how can I have my properties show in this list and be able to change their values, does it have to be a List to be in a List?
I just want to be able to modify these properties from a Drop-down list to toggle the Visibility of the ShowEventStart and ShowEventEnd Property Values using the Checkboxes in the Drop-down List.
Plus this must be a Silverlight 3.0 solution, I cannot figure out how to have something that can be bound in the XAML which is not a list and then bound it as a list to modify these items!
I just need a list of Checkboxes which alter the values of the Class Properties such as ShowEventStart and ShowEventEnd, there are other properties but this will be a start.
You can create a PropertyWrapper class and in you window code behind expose a property that returns a List<PropertyWrapper> and bind your ListBox.ItemsSource to it.
public class PropertyWrapper
{
private readonly object target;
private readonly PropertyInfo property;
public PropertyWrapper(object target, PropertyInfo property)
{
this.target = target;
this.property = property;
}
public bool Value
{
get
{
return (bool) property.GetValue(target, null);
}
set
{
property.SetValue(target, value, null);
}
}
public PropertyInfo Property
{
get
{
return this.property;
}
}
}
your window code behind:
public partial class Window1 : Window, INotifyPropertyChanged
{
public Window1()
{
InitializeComponent();
properties = new List<PropertyWrapper>
{
new PropertyWrapper(this, typeof(Window1).GetProperty("A")),
new PropertyWrapper(this, typeof(Window1).GetProperty("B")),
};
this.DataContext = this;
}
private List<PropertyWrapper> properties;
public List<PropertyWrapper> Properties
{
get { return properties; }
}
private bool a;
private bool b = true;
public bool A
{
get
{
return a;
}
set
{
if (value != a)
{
a = value;
NotifyPropertyChange("A");
}
}
}
public bool B
{
get
{
return b;
}
set
{
if (value != b)
{
b = value;
NotifyPropertyChange("B");
}
}
}
protected void NotifyPropertyChange(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged.Invoke(
this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
your window markup:
<ListBox ItemsSource="{Binding Path=Properties}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Property.Name}"/>
<CheckBox IsChecked="{Binding Path=Value}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Hope this helps
I tried to mock up something similar. See how this works for you and let me know if I misunderstood the question.
From MainPage.xaml:
<StackPanel Orientation="Horizontal">
<ListBox SelectedItem="{Binding ShowControls, Mode=TwoWay}" x:Name="VisibilityList"/>
<Button Content="Test" Visibility="{Binding ShowControls}"/>
<CheckBox Content="Test 2" Visibility="{Binding ShowControls}"/>
</StackPanel>
The code behind MainPage.xaml.cs:
public partial class MainPage : UserControl
{
VisibilityData visibilityData = new VisibilityData();
public MainPage()
{
InitializeComponent();
VisibilityList.Items.Add(Visibility.Visible);
VisibilityList.Items.Add(Visibility.Collapsed);
this.DataContext = visibilityData;
}
}
And the data class:
public class VisibilityData : INotifyPropertyChanged
{
private Visibility showControls = Visibility.Visible;
public Visibility ShowControls
{
get { return showControls; }
set
{
showControls = value;
OnPropertyChanged("ShowControls");
}
}
private void OnPropertyChanged(string p)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(p));
}
public event PropertyChangedEventHandler PropertyChanged;
}
When you run that code you should get a ListBox with the options Visible and Collapsed and when you choose an option you should see the visibility of the button and checkbox is changed to reflect your choice. Let me know if that's not what you were looking for.
After thinking about this the solution occured to me, which is to construct the List in XAML then using a Converter on the Checkboxes to convert their Boolean IsChecked to the Property Visibility Property in the Class.
You can create a list such as:
<ComboBox Canvas.Left="6" Canvas.Top="69" Width="274" Height="25" Name="Display">
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding Path=Display.EventStart,Mode=TwoWay,Converter={StaticResource VisibilityConverter}}"/>
<TextBlock Text="Event Start"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding Path=Display.EventEnd,Mode=TwoWay,Converter={StaticResource VisibilityConverter}}"/>
<TextBlock Text="Event End"/>
</StackPanel>
</ComboBox>
I can then Bind to the Two Properties I want (this example from the actual application).