x:Bind will leak memory when Binding DataContext in xaml - c#

First of all, I have two pages called MainPage and BindPage. The MainPage can navigate to the BindPage. The BindPage have two bindings:
One is DataContext Binding in xaml:
<Page x:Class="MvvmlightTest.View.BindingPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MvvmlightTest.View"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:cvt="using:MvvmlightTest.Converter"
DataContext="{Binding BindingVM, Source={StaticResource Locator}, Mode=OneWay}"
Unloaded="Page_Unloaded">
The other binding is in the page content:
<Grid Background="#7F919191"
Visibility="{x:Bind VM.IsInBuyProcess, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}">
<ProgressRing Width="100"
Height="100"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsActive="True" />
</Grid>
The definition of VM is:
public BindingViewModel VM
{
get
{
return DataContext as BindingViewModel;
}
}
The BindingViewModel has just one property:
private bool isInBuyProcess;
public bool IsInBuyProcess
{
get
{
return true;
}
set
{
isInBuyProcess = value;
}
}
And also I need to show BindPage.g.cs, it contain two important classes: BindingPage_obj1_Bindings & BindingPage_obj1_BindingsTracking
BindingPage_obj1_BindingsTracking is the member of BindingPage_obj1_Bindings
public BindingPage_obj1_BindingsTracking bindingsTracking;
public BindingPage_obj1_Bindings()
{
this.bindingsTracking = new BindingPage_obj1_BindingsTracking(this);
Tk = new WeakReference<BindingPage_obj1_BindingsTracking>(this.bindingsTracking);
}
The BindingPage_obj1_Bindings is initialize here:
public global::Windows.UI.Xaml.Markup.IComponentConnector GetBindingConnector(int connectionId, object target)
{
global::Windows.UI.Xaml.Markup.IComponentConnector returnValue = null;
switch(connectionId)
{
case 1:
{
global::Windows.UI.Xaml.Controls.Page element1 = (global::Windows.UI.Xaml.Controls.Page)target;
BindingPage_obj1_Bindings bindings = new BindingPage_obj1_Bindings();
returnValue = bindings;
bindings.SetDataRoot(this);
bindings.SetConverterLookupRoot(this);
this.Bindings = bindings;
element1.Loading += bindings.Loading;
}
break;
}
return returnValue;
}
And BindingPage_obj1_BindingsTracking could not be released outside of BindingPage_obj1_Bindings
private void StopTracking()
{
this.bindingsTracking.ReleaseAllListeners();
this.initialized = false;
}
private void ReleaseAllListeners()
{
UpdateChildListeners_VM(null);
}
private void UpdateChildListeners_VM(global::MvvmlightTest.ViewModel.BindingViewModel obj)
{
if (obj != cache_VM)
{
if (cache_VM != null)
{
((global::System.ComponentModel.INotifyPropertyChanged)cache_VM).PropertyChanged -= PropertyChanged_VM;
cache_VM = null;
}
if (obj != null)
{
cache_VM = obj;
((global::System.ComponentModel.INotifyPropertyChanged)obj).PropertyChanged += PropertyChanged_VM;
}
}
}
After I navigate From MainPage to BindPage, and then go back with GC collection, the BindPage and BindingPage_obj1_Bindings can be released but BindingPage_obj1_BindingsTracking NOT.
If I move the DataContext binding to code-behind and write it to the Page_Loaded event, BindingPage_obj1_BindingsTracking will be released.
So, does the result mean the x:Bind could not be used with Binding?
btw, my test project here
======================Update======================
After some days, I find the result. The solution is quite simple, just call the Bindings.StopTracking() in the Page_Unloaded method, so all the one-way binding and two way binding should be released.
The solution is not a good way to implement more with MVVM because the Bindings property is private and we need to call the function in the code-behind. I don't quite understand why Microsoft generate the Bindings to private.

Related

How to build a custom control using recursion?

I am trying to build a rather complex custom control.
I want to use recursion in my View Model to build out a control. I will try and be as clear as possible.
I have two classes, Publisher and Developer
The Publisher class looks like the following:
public class Publisher
{
public Publisher()
{
SubPublishers.CollectionChanged += SubPublishers_CollectionChanged;
ChildDevelopers.CollectionChanged += SubPublishers_CollectionChanged;
}
private ObservableCollection<Publisher> subPublishers;
public ObservableCollection<Publisher> SubPublishers
{
get
{
if (subPublishers == null)
subPublishers = new ObservableCollection<Publisher>();
return subPublishers;
}
}
private ObservableCollection<Developer> childDevelopers;
public ObservableCollection<Developer> ChildDevelopers
{
get
{
if (childDevelopers == null)
childDevelopers = new ObservableCollection<Developer>();
return childDevelopers;
}
}
And my Developer Class looks like this:
public class Developer : NotifyPropertyChanged
{
public Developer(string Title)
{
this.Title = Title;
}
private string title;
public string Title
{
get
{
return this.title;
}
set
{
this.title = value;
OnPropertyChanged("Title");
}
}
So yes, Publisher is n-tier. It can have a Collection of Developers and each of these Developers can have their own Collection of Developers.
Going to my Main View Model:
public class MainViewModel : NotifyPropertyChanged
{
public MainViewModel()
{
this.ParentPublisher = new ParentPublisher();
BuildData();
}
private Publisher parentPublisher;
public Publisher ParentPublisher
{
get
{
return this.parentPublisher;
}
set
{
this.parentPublisher = value;
OnPropertyChanged("ParentPublisher");
}
}
public void BuildData()
{
Publisher firstPublisher = new Publisher();
firstPublisher.ChildDevelopers.Add(new Developer("HAL"));
firstPublisher.ChildDevelopers.Add(new Developer("Retro Games"));
firstPublisher.ChildDevelopers.Add(new Developer("Nintendo"));
Publisher secondPublisher = new Publisher();
secondPublisher.ChildDevelopers.Add(new Developer("343"));
secondPublisher.ChildDevelopers.Add(new Developer("Playground Games"));
secondPublisher.SubPublishers.Add(new Publisher());
secondPublisher.SubPublishers.FirstOrDefault().ChildDevelopers.Add(new Developer("Coalition"));
secondPublisher.SubPublishers.FirstOrDefault().ChildDevelopers.Add(new Developer("Remedy"));
secondPublisher.SubPublishers.FirstOrDefault().SubPublishers.Add(new Publisher());
secondPublisher.SubPublishers.FirstOrDefault().SubPublishers.FirstOrDefault().ChildDevelopers.Add(new Developer("Insomniac"));
secondPublisher.SubPublishers.FirstOrDefault().SubPublishers.FirstOrDefault().ChildDevelopers.Add(new Developer("Criterion"));
secondPublisher.SubPublishers.FirstOrDefault().SubPublishers.FirstOrDefault().ChildDevelopers.Add(new Developer("EA"));
ParentPublisher.Add(firstPublisher);
ParentPublisher.Add(secondPublisher);
}
}
}
So, you can see the possible scenarios here. Now, I was trying to figure out how to build a control off this data.
I want to actually bind to the ParentPublisher because everything added (SubPublishers and the Child Developers) will ultimately be extension of the Parent.
Would I use an ObservableCollection and use the ItemSource to this ParentPublisher?
Any tips or recommendations would be appreciated.
One way is to make a UserControl that excepts the first tier as a DataTemplate. Then you show the data in the DataTemplate as needed. Inside the DataTemplate reference the UserControl again with the inner data.
Example: Obviously modify the layout to benefit you but for simplicity I did it this way.
<UserControl x:Class="WPF_Question_Answer_App.PublisherView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPF_Question_Answer_App"
mc:Ignorable="d"
d:DesignHeight="450"
d:DesignWidth="800">
<UserControl.Resources>
<DataTemplate x:Key="DeveloperTemplate">
<TextBlock Text="{Binding Title}" /> <!--show the developer data-->
</DataTemplate>
<DataTemplate x:Key="PublisherTemplate">
<local:PublisherView /> <!-- reference ourself for recursion-->
</DataTemplate>
</UserControl.Resources>
<StackPanel>
<ItemsControl ItemsSource="{Binding ChildDevelopers}"
ItemTemplate="{StaticResource DeveloperTemplate}" />
<ItemsControl ItemsSource="{Binding SubPublishers}"
ItemTemplate="{StaticResource PublisherTemplate}" />
</StackPanel>
</UserControl>

WPF & SimpleMVVM: Binding new viewmodel to view

Please note that this question is specific to SimpleMVVM and the use of its ViewModelLocator.
I have a view setup like so:
<UserControl x:Class="CallTracker.WPF.Views.CallUserControl"
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"
DataContext="{Binding Source={StaticResource Locator}, Path=CallViewModel}">
<StackPanel>
<TextBlock Text="{Binding GreetingText}" />
</StackPanel>
</UserControl>
And my locator defines the CallViewModel as:
// Create CallViewModel on demand
public CallViewModel CallViewModel
{
get
{
return _CallViewModel;
}
set
{
if (_CallViewModel != value)
{
_CallViewModel = value;
}
}
}
private CallViewModel _CallViewModel;
During execution I am trying to create a new CallUserControl to use but, using Snoops, I can see that the datacontext of CallUserControl is not being bound to the viewmodel (the datacontext in Snoops says null).
Stepping thru the code I can see that the CallViewModel is getting created and assigned to the object within the locator but the UserControl's datacontext is not binding to this newly created object.
I am creating the new CallViewModel as such:
private void NewCall()
{
MessageBox.Show("NewCallCommand executed.");
// Only start a new call if one doesn't currently exist
if((App.Current.Resources["Locator"] as ViewModelLocator).CallViewModel == null)
{
CurrentCallViewModel = new CallViewModel();
}
}
....
/// <summary>
/// Current MainContentViewModel
/// </summary>
public CallViewModel CurrentCallViewModel
{
get
{
//Visibility _visible = (new ViewModelLocator()).CallViewModel == null ? Visibility.Collapsed : Visibility.Visible;
return (App.Current.Resources["Locator"] as ViewModelLocator).CallViewModel;
}
set
{
if (this._CurrentCallViewModel != value)
{
this._CurrentCallViewModel = value;
(App.Current.Resources["Locator"] as ViewModelLocator).CallViewModel = value;
NotifyPropertyChanged(m => m.CurrentCallViewModel);
}
}
}
private CallViewModel _CurrentCallViewModel = null;
Anyyou know why this new ViewModel would not bind to the new Model? Is there some mechanism similar to NotifyPropertyChanged that I have to call from the locator to get the objects to bind?

How to unselect selected row in LongListSelector in WP8

I must say that I really dislike the LongListSelector in WP8 and like the toolkit version so much better.
First it is not MVVM compatible so I found this code to make it so.
public class LongListSelector : Microsoft.Phone.Controls.LongListSelector
{
public LongListSelector()
{
SelectionChanged += LongListSelector_SelectionChanged;
}
void LongListSelector_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
SelectedItem = base.SelectedItem;
}
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register(
"SelectedItem",
typeof(object),
typeof(LongListSelector),
new PropertyMetadata(null, OnSelectedItemChanged)
);
private static void OnSelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var selector = (LongListSelector)d;
selector.SelectedItem = e.NewValue;
}
public new object SelectedItem
{
get { return GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
}
then I made a view model using mvvm light
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
MyList = new ObservableCollection<Test>
{
new Test
{
Name = "test 1"
},
new Test
{
Name = "test 2"
}
};
ButtonCmd = new RelayCommand(() => Hit());
}
private void Hit()
{
SelectedItem = null;
}
public ObservableCollection<Test> MyList { get; set; }
/// <summary>
/// The <see cref="SelectedItem" /> property's name.
/// </summary>
public const string SelectedItemPropertyName = "SelectedItem";
private Test selectedItem = null;
/// <summary>
/// Sets and gets the SelectedItem property.
/// Changes to that property's value raise the PropertyChanged event.
/// </summary>
public Test SelectedItem
{
get
{
return selectedItem;
}
set
{
if (value != null)
{
MessageBox.Show(value.Name);
}
if (selectedItem == value)
{
return;
}
RaisePropertyChanging(() => SelectedItem);
selectedItem = value;
RaisePropertyChanged(() => SelectedItem);
}
}
public RelayCommand ButtonCmd
{
get;
private set;
}
then I made a model
public class Test : ObservableObject
{
public string Name { get; set; }
}
then I made the xaml
<phone:PhoneApplicationPage
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ignore="http://www.ignore.com"
xmlns:local="clr-namespace:MvvmLight2" x:Class="MvvmLight2.MainPage"
mc:Ignorable="d ignore"
FontFamily="{StaticResource PhoneFontFamilyNormal}"
FontSize="{StaticResource PhoneFontSizeNormal}"
Foreground="{StaticResource PhoneForegroundBrush}"
SupportedOrientations="Portrait"
Orientation="Portrait"
shell:SystemTray.IsVisible="True"
DataContext="{Binding Main, Source={StaticResource Locator}}">
<!--LayoutRoot is the root grid where all page content is placed-->
<Grid x:Name="LayoutRoot"
Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<local:LongListSelector ItemsSource="{Binding MyList}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
<local:LongListSelector.Resources>
<DataTemplate x:Key="ItemTemplate">
<Grid>
<TextBlock Text="{Binding Name}" HorizontalAlignment="Left" VerticalAlignment="Top" FontSize="48"/>
</Grid>
</DataTemplate>
</local:LongListSelector.Resources>
<local:LongListSelector.ItemTemplate>
<StaticResource ResourceKey="ItemTemplate"/>
</local:LongListSelector.ItemTemplate>
</local:LongListSelector>
<Button Content="Unselect" HorizontalAlignment="Left" Margin="142,81,0,0" Grid.Row="1" VerticalAlignment="Top" Command="{Binding ButtonCmd, Mode=OneWay}"/>
</Grid>
</phone:PhoneApplicationPage>
when I click on the first item in the list, the message box shows up, if I hit it again nothing happens. I then hit my button which nulls out the selectedItem(which in the old toolkit version would be enough) and try again and nothing happens.
Only way I can every select the first row is by selecting the second row which is really bad if say the list only has 1 item at any given time.
Weird thing is that a simple collection of strings does not even require me to set the SelectItem to null as it always seems to deselect but when it comes to complex types it is a no go.
I've used the same code and ran into the very same problem. Reason for it is that your SelectedItem property overshadows the base.SelectedItem property. When setting it to a new value, not only set your SelectedItem property but the base one as well:
public new object SelectedItem
{
get { return GetValue(SelectedItemProperty); }
set
{
SetValue(SelectedItemProperty, value);
base.SelectedItem = value;
}
}
Then you have a MVVM capable code and can reset the SelectedItem in your ViewModel as well (by setting it to null).
you can easily achieve via in selectionchanged event
void LongListSelector_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if((sender as LongListSelector).SelectedItem == null){
return;
}
SelectedItem = base.SelectedItem;
(sender as LongListSelector).SelectedItem = null;
}

View within a view not updating - Caliburn.Micro

I'm having an issue where a datagrid is not reflecting changes to its collection when attached to a view inside a view. More accurately, I have a SecondView within the MainView. On the SecondView I have a datagrid with autogeneratecolumns set to true; when the datagrid is first rendered, it displays the appropriate columns and headers. However, when I populate the list that is attached to it, no changes are reflected.
Here is the complete code for the two views and their respective viewmodels:
MainWindowView:
<Window x:Class="MyApp.MainWindowView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:cal="http://www.caliburnproject.org"
xmlns:views="clr-namespace:MyApp"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindowView" Height="300" Width="300">
<Grid>
<DockPanel>
<Menu DockPanel.Dock="Top">
<MenuItem Header="File">
<MenuItem Header="Open" x:Name="Open"/>
<MenuItem Header="Exit" x:Name="Exit"/>
</MenuItem>
</Menu>
<StackPanel DockPanel.Dock="Bottom">
<views:SecondView/>
</StackPanel>
</DockPanel>
</Grid>
MainWindowViewModel:
namespace MyApp
{
[Export(typeof(IShell))]
internal class MainWindowViewModel : Screen, IShell
{
Regex expression = new Regex(#"^N\d\.C\d\.D\d\.R\d:\s\s\s-\d"); //ex. "N1.C1.D2.R1: -3"
SecondViewModel svm = new SecondViewModel();
public void Open()
{
Microsoft.Win32.OpenFileDialog openFile = new Microsoft.Win32.OpenFileDialog();
openFile.Multiselect = true;
openFile.Filter = "Text Files(*.txt)|*.txt|Log Files(*.log)|*.log|All Files(*.*)|*.*";
openFile.Title = "Open File(s)";
bool? userClickedOK = openFile.ShowDialog();
string[] _fileNames = openFile.FileNames;
if (userClickedOK == true)
{
if (_fileNames != null)
{
for (int i = 0; i < _fileNames.Length; i++)
{
ValidFiles(_fileNames[i]);
}
}
}
}
public void Exit()
{
App.Current.Shutdown();
}
/* ValidFiles() accepts a string containing a filename and creates a Streamreader that reads the file if it is not a Boxboro file.
*/
public void ValidFiles(string filename)
{
string line;
using (StreamReader sr = new StreamReader(filename))
{
while ((line = sr.ReadLine()) != null)
{
if (line.Contains("Mono Status"))
{
Console.WriteLine("File(s) not supported by this parser. Please select a valid file.");
break;
}
else
{
IsMatch(line);
}
}
}
}
/* IsMatch() accepts a string "input" and determines which parsing method to send the string to, if any.
* Strings not matching any of the initial criteria are not processed to limit overhead.
*/
public void IsMatch(string input)
{
Match match = expression.Match(input);
if (match.Success)
{
svm.GetData(input);
}
}
}
}
SecondWindowView:
<UserControl x:Class="MyApp.SecondView"
xmlns:cal="http://www.caliburnproject.org"
cal:Bind.Model="MyApp.SecondViewModel"
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"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<StackPanel>
<DataGrid x:Name="MyList"/>
</StackPanel>
</Grid>
SecondWindowViewModel:
namespace MyApp
{
[Export(typeof(SecondViewModel))]
class SecondViewModel:Screen
{
Parse parse = new Parse();
BindableCollection<MyObject> myList = new BindableCollection<MyObject>();
MyObject myObject;
public MyObject MyObject
{
get { return myObject; }
set
{
myObject = value;
NotifyOfPropertyChange(() => MyList);
}
}
public BindableCollection<MyObject> MyList
{
get { return myList; }
set
{
MyList = value;
NotifyOfPropertyChange(() => MyList);
}
}
public void GetData(string input)
{
string[] tempArray = input.Split();
List<int> tempList = new List<int>();
for (int i = 1; i < tempArray.Length; i++)
{
if (!string.IsNullOrEmpty(tempArray[i]))
{
tempList.Add(Convert.ToInt32(tempArray[i]));
}
}
int[] tempIntArray = tempList.ToArray();
MyObject = new MyObject(tempArray[0], tempIntArray[0], tempIntArray[1], tempIntArray[2], tempIntArray[3]);
this.MyList.Add(MyObject);
Console.WriteLine("MyList has " + MyList.Count.ToString() + " elements.");
//foreach (MyObject item in MyList)
//{
// Console.WriteLine(item.Location);
//}
}
}
}
Boostrapper:
namespace MyApp
{
internal class AppBootStrapper : Bootstrapper<IShell>
{
static AppBootStrapper()
{
//Initializes the logger for debugging, remove or comment out in release.
LogManager.GetLog = type => new DebugLogger(type);
}
private CompositionContainer container;
protected override void BuildUp(object instance)
{
this.container.SatisfyImportsOnce(instance);
}
protected override void Configure()
{
this.container =
new CompositionContainer(
new AggregateCatalog(
AssemblySource.Instance.Select(x => new AssemblyCatalog(x)).OfType<ComposablePartCatalog>()));
var batch = new CompositionBatch();
batch.AddExportedValue<IWindowManager>(new WindowManager());
batch.AddExportedValue<IEventAggregator>(new EventAggregator());
batch.AddExportedValue(this.container);
this.container.Compose(batch);
}
protected override IEnumerable<object> GetAllInstances(Type serviceType)
{
return this.container.GetExportedValues<object>(AttributedModelServices.GetContractName(serviceType));
}
//This method is required for the BootStrapper.cs to be discovered.
protected override object GetInstance(Type serviceType, string key)
{
string contract = string.IsNullOrEmpty(key) ? AttributedModelServices.GetContractName(serviceType) : key;
IEnumerable<object> exports = this.container.GetExportedValues<object>(contract);
if (exports.Count() > 0)
{
return exports.First();
}
throw new Exception(string.Format("Could not locate any instances of contract {0}.", contract));
}
}
}
Based on my understanding of Caliburn.Micro, whenever the observablecollection MyList is updated (a new item added), the datagrid with x:name MyList should be updated. Even without a data template, I would think I would see a list of blank entries equivalent in length to the number of objects in MyList. When I use this same code in the MainViewModel, rather than in a usercontrol bound to the MainView, I have no issues rendering the list. It seems I'm missing something about updating a view within a view.
I should note, I can verify the list has objects in it by using Console.WriteLine(MyList.Count.ToString()) and watching the output window. I hate asking about these things, every time I do it ends up being a typo or something equally silly, but I've been stuck here for too long.
NOTE: Even with MyList.Refresh() thrown in on each iteration, no changes in the datagrid occur.
NOTE: It seems like this may answer my question, but I don't understand how to implement it. Perhaps if someone else understands it better, they could put the lines of code in the appropriate places in my code and explain why it works. Thanks in advance. Caliburn.Micro convention-based bindings not working in nested views?
Try this viewmodel first approach - I suspect your inner view isn't being bound (CM doesn't look across control boundaries when applying conventions e.g. it won't apply conventions to nested usercontrols)
<Window x:Class="MyApp.MainWindowView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:cal="http://www.caliburnproject.org"
xmlns:views="clr-namespace:MyApp"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindowView" Height="300" Width="300">
<Grid>
<DockPanel>
<Menu DockPanel.Dock="Top">
<MenuItem Header="File">
<MenuItem Header="Open" x:Name="Open"/>
<MenuItem Header="Exit" x:Name="Exit"/>
</MenuItem>
</Menu>
<StackPanel DockPanel.Dock="Bottom">
<!-- Use ContentControl for sub-views, CM will do it's magic if you bind to the VM property using the standard conventions -->
<ContentControl x:Name="SecondView" />
</StackPanel>
</DockPanel>
</Grid>
Then in your main:
internal class MainWindowViewModel : Screen, IShell
{
Regex expression = new Regex(#"^N\d\.C\d\.D\d\.R\d:\s\s\s-\d"); //ex. "N1.C1.D2.R1: -3"
// Declare your second VM as a property so you can bind to it via CM conventions
public SecondViewModel SecondView
{
get { return _secondView; }
set
{
_secondView = value;
NotifyOfPropertyChange(() => SecondView);
}
}
public MainWindowViewModel()
{
SecondView = new SecondViewModel();
}
CM will automatically inject the right view into the content controls template and setup the datacontext
Alternatively, you can use Bind.Model to bind the VM instance to the view which is more a view-first approach
<StackPanel DockPanel.Dock="Bottom">
<views:SecondView cal:Bind.Model="{Binding SecondView}" />
</StackPanel>
(I think it's Bind.Model and not View.Model but I often get the two mixed up, so failing Bind.Model try View.Model)

WPF ComboBox Binding works if previous record had no matching item in the ItemsSource collection and fails if it did

I have the following WPF Combobox:
<Window.Resources>
<CollectionViewSource x:Key="performanceItemsource" Source="{Binding Path=SelectedReport.Performances}" >
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="Name"/>
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
</Window.Resources>
...
<ComboBox Name="cbxPlanPerf" Grid.ColumnSpan="2"
SelectedValuePath="MSDPortfolioID" DisplayMemberPath="Name"
SelectedValue="{Binding Path=PlanPerfID}"
ItemsSource="{Binding Source={StaticResource performanceItemsource}}"/>
The Source for the CollectionViewSource is:
public List<MSDExportProxy> Performances
{
get
{
if (Portfolio != null)
{
return (from a in Portfolio.Accounts where a.MSDPortfolioID != null select new MSDExportProxy(a))
.Concat<MSDExportProxy>(from g in Portfolio.Groups where g.MSDPortfolioID != null select new MSDExportProxy(g))
.Concat<MSDExportProxy>(from p in new[] { Portfolio } where p.MSDPortfolioID != null select new MSDExportProxy(p))
.ToList<MSDExportProxy>();
}
return new List<MSDExportProxy>();
}
}
The bound property PlanPerfID is a string.
I move between records using a ListBox control. The ComboBox works fine if the previous record had no items in its ComboBox.ItemsSource. If there were any items in the previous record's ComboBox.ItemsSource then the new record won't find its matching item in the ItemsSource collection. I've tried setting the ItemsSource in both XAML and the code-behind, but nothing changes this odd behavior. How can I get this darn thing to work?
Try using ICollectionViews in combination with IsSynchronizedWithCurrentItem property when handling lists / ObservableCollection in Xaml. The ICollectionView in the viewmodel can handle all the things needed, e.g. sorting, filtering, keeping track of selections and states.
Xaml:
<Window x:Class="ComboBoxBinding.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">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ListBox Grid.Column="0"
ItemsSource="{Binding Reports}"
DisplayMemberPath="Name"
IsSynchronizedWithCurrentItem="True" />
<ComboBox Grid.Column="1"
ItemsSource="{Binding CurrentReport.Performances}"
DisplayMemberPath="Name"
IsSynchronizedWithCurrentItem="True" />
</Grid>
</Window>
ViewModel:
public class ViewModel : INotifyPropertyChanged
{
private readonly IReportService _reportService;
private ObservableCollection<ReportViewModel> _reports = new ObservableCollection<ReportViewModel>();
private PerformanceViewModel _currentPerformance;
private ReportViewModel _currentReport;
public ObservableCollection<ReportViewModel> Reports
{
get { return _reports; }
set { _reports = value; OnPropertyChanged("Reports");}
}
public ReportViewModel CurrentReport
{
get { return _currentReport; }
set { _currentReport = value; OnPropertyChanged("CurrentReport");}
}
public PerformanceViewModel CurrentPerformance
{
get { return _currentPerformance; }
set { _currentPerformance = value; OnPropertyChanged("CurrentPerformance");}
}
public ICollectionView ReportsView { get; private set; }
public ICollectionView PerformancesView { get; private set; }
public ViewModel(IReportService reportService)
{
if (reportService == null) throw new ArgumentNullException("reportService");
_reportService = reportService;
var reports = _reportService.GetData();
Reports = new ObservableCollection<ReportViewModel>(reports);
ReportsView = CollectionViewSource.GetDefaultView(Reports);
ReportsView.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
ReportsView.CurrentChanged += OnReportsChanged;
ReportsView.MoveCurrentToFirst();
}
private void OnReportsChanged(object sender, EventArgs e)
{
var selectedReport = ReportsView.CurrentItem as ReportViewModel;
if (selectedReport == null) return;
CurrentReport = selectedReport;
if(PerformancesView != null)
{
PerformancesView.CurrentChanged -= OnPerformancesChanged;
}
PerformancesView = CollectionViewSource.GetDefaultView(CurrentReport.Performances);
PerformancesView.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
PerformancesView.CurrentChanged += OnPerformancesChanged;
PerformancesView.MoveCurrentToFirst();
}
private void OnPerformancesChanged(object sender, EventArgs e)
{
var selectedperformance = PerformancesView.CurrentItem as PerformanceViewModel;
if (selectedperformance == null) return;
CurrentPerformance = selectedperformance;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
I found a quick and dirty solution to my problem. I just happen to have a public NotifyPropertyChanged() method on my Report entity and I discovered that if I called SelectedReport.NotifyPropertyChanged("PlanPerfID") in the Report ListBox's SelectionChanged event that it was enough of a jolt to get the ComboBox to re-evaluate and find its matching item in the ItemsSource. Yeah, it's KLUGE...
UPDATE: I also wound up needing to add SelectedReport.NotifyPropertyChanged("Performances") for some situations...
UPDATE 2: Okay, turns out the above wasn't bullet proof and I ran across a situation that broke it so I had to come up with a better workaround:
Altered the SelectedReport property in the Window's code-behind, adding a private flag (_settingCombos) to keep the Binding from screwing up the bound values until the dust has settled from changin the ItemSource:
private bool _settingCombos = false;
private Report _SelectedReport;
public Report SelectedReport
{
get { return _SelectedReport; }
set
{
_settingCombos = true;
_SelectedReport = value;
NotifyPropertyChanged("SelectedReport");
}
}
Created a proxy to bind to in the Window code-behind that will refuse to update the property's value if the _settingCombos flag is true:
public string PlanPerfID_Proxy
{
get { return SelectedReport.PlanPerfID; }
set
{
if (!_settingCombos)
{
SelectedReport.PlanPerfID = value;
NotifyPropertyChanged("PlanPerfID_Proxy");
}
}
}
Added an extra Notification in the Report ListBox's SelectionChanged event along with code to reset the _settingCombos flag back to false:
private void lbxReports_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
//KLUGE: Couldn't get the ComboBoxes associated with these properties to work right
//this forces them to re-evaluate after the Report has loaded
if (SelectedReport != null)
{
NotifyPropertyChanged("PlanPerfID_Proxy");
_settingCombos = false;
}
}
Bound the ComboBox to the PlanPerfID_Proxy property (instead of directly to the SelectedReport.PlanPerfID property.
Wow, what a hassle! I think that this is simply a case of .NET's binding logic getting confused by the dynamic nature of the ComboBox.ItemSource, but this seems to have fixed it. Hope it helps someone else.

Categories

Resources