DependencyProperty ObservableCollection event - wpf

I am trying to make UserControl (GridSearch) which has another UserControl_2 inside. I want to add some FrameworkElements to panel of UserControl_2 using XAML.
So I did ObservableCollection DependencyProperty in GridSearch:
public partial class GridSearch : UserControl
{
public GridSearch()
{
InitializeComponent();
}
public ObservableCollection<Filter> Filters
{
get { return (ObservableCollection<Filter>)GetValue(FiltersProperty); }
set { SetValue(FiltersProperty, value); }
}
public static readonly DependencyProperty FiltersProperty =
DependencyProperty.Register("Filters",
typeof(ObservableCollection<Filter>),
typeof(GridSearch),
new FrameworkPropertyMetadata(getObservableFilters(), null)
);
private static ObservableCollection<Filter> getObservableFilters()
{
var ob = new ObservableCollection<Filter>();
ob.CollectionChanged += ob_CollectionChanged;
return ob;
}
private static void ob_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
}
}
Now I was trying to add new elements to the panel by using ob_CollectionChanged. However because it is static method I cannot access the panel. I cannot cast the sender because it gives me only the ObservableCollection. However I need GridSearch.
I am looking for solution from several hours and I am not able to find any idea how to solve it.

Change the getObservableFilters() method to just create and return the observable collection.
And in the GridSearch() constructor, after the call to InitializeComponent(), you can add a handler for Filters.CollectionChanged and provide a non-static member function.

Ok it works finally the key thing was to create new ObservableCollection() in constructor for each instance of the control.
However there is still one problem. Everything works at runtime but designer is not able to display anything as it gets following error:
Object reference not set to an instance of an object.
and this is tabout this line: <gsh:GridSearch.Filters>
This is the code I end up with:
public partial class GridSearch : UserControl
{
public GridSearch()
{
Filters = new ObservableCollection<Label>();
InitializeComponent();
Filters.CollectionChanged += Filters_CollectionChanged;
}
void Filters_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
foreach (Label uc in e.NewItems)
pnlFilters.Children.Add(uc);
}
public ObservableCollection<Label> Filters
{
get { return (ObservableCollection<Label>)GetValue(FiltersProperty); }
set { SetValue(FiltersProperty, value); }
}
public static readonly DependencyProperty FiltersProperty =
DependencyProperty.Register("Filters",
typeof(ObservableCollection<Label>),
typeof(GridSearch),
new FrameworkPropertyMetadata(new ObservableCollection<Label>(), null)
);
}
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="0.5*"/>
<RowDefinition Height="0.5*"/>
</Grid.RowDefinitions>
<gsh:GridSearch>
<gsh:GridSearch.Filters>
<Label Content="aa" />
<Label Content="aa" />
<Label Content="aa" />
</gsh:GridSearch.Filters>
</gsh:GridSearch>
<gsh:GridSearch Grid.Row="1">
<gsh:GridSearch.Filters>
<Label Content="bb" />
<Label Content="cc" />
<Label Content="dd" />
</gsh:GridSearch.Filters>
</gsh:GridSearch>
</Grid>

Related

Catel ViewToViewModel attribute

Thank you so much for your help.
I'm trying to understand the ViewToViewModel attribute by getting a small example to work. I've go a couple of questions. My code is below.
Is the [ViewToViewModel] attribute supposed to be placed to be placed in the View, ViewModel or both?
If I try to use an attribute, MappingType, such as: [ViewToViewModel, MappingType = ...] MappingType gives me an error. Am I missing a "using" statement/Assembly Reference? Is there an example of syntax?
I'm able to get things to work the way I need, but I don't think that I'm getting the "ViewToViewModel" part to work properly. In the codebehind of the usercontrol, property changes are handled in HandleMyName(object e). Is ViewToViewModel supposed to do this?
Views:
MainWindow
UserControlView
ViewModels:
MainwindowViewModel
UserControlViewViewmodel
MainWindow
<catel:DataWindow x:Class="ViewToViewModelStudy.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:catel="http://catel.codeplex.com"
xmlns:uc="clr-namespace:ViewToViewModelStudy.Views" >
<StackPanel x:Name="LayoutRoot">
<Label Content="{Binding Title}" />
<uc:UserControlView MyName="{Binding Title}" />
</StackPanel>
</catel:DataWindow>
.
UserControlView.xaml
<catel:UserControl x:Class="ViewToViewModelStudy.Views.UserControlView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:catel="http://catel.codeplex.com">
<StackPanel>
<TextBlock>Innerview Model</TextBlock>
<TextBlock Text="{Binding MyName}"></TextBlock>
<TextBlock>Innerview Model</TextBlock>
</StackPanel>
</catel:UserControl>
UserControlView.xaml.cs
namespace ViewToViewModelStudy.Views
{
using Catel.Windows.Controls;
using Catel.MVVM.Views;
using System.Windows;
using System.Data;
public partial class UserControlView : UserControl
{
[ViewToViewModel]
public string MyName
{
get { return (string)GetValue(MyNameProperty); }
set { SetValue(MyNameProperty, value); }
}
public static readonly DependencyProperty MyNameProperty =
DependencyProperty.Register(
"MyName",
typeof(string),
typeof(UserControlView),
new FrameworkPropertyMetadata(null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, new PropertyChangedCallback(OnMyName)));
static void OnMyName(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
UserControlView ic = (UserControlView)obj;
ic.HandleMyName(e.NewValue);
}
private void HandleMyName(object e)
{
ViewModels.UserControlViewModel vm = (ViewModels.UserControlViewModel)this.ViewModel;
if (vm != null)
{
vm.MyName = e.ToString(); // << Shouldn't this happen automagically?
}
}
public UserControlView()
{
InitializeComponent();
}
}
}
UserControlViewModel.cs
namespace ViewToViewModelStudy.ViewModels
{
using Catel.MVVM;
using Catel.Data;
using Catel.MVVM.Views;
using Catel.Windows.Controls;
public class UserControlViewModel : ViewModelBase
{
public UserControlViewModel()
{ }
public string MyName
{
get { return GetValue<string>(MyNameProperty); }
set { SetValue(MyNameProperty, value); }
}
public static readonly PropertyData MyNameProperty = RegisterProperty("MyName", typeof(string), null, (sender, e) => ((UserControlViewModel)sender).OnMyPropertyChanged());
private void OnMyPropertyChanged()
{
}
}
}
1) A ViewToViewModel should be located in the view (you don't want to pollute your VM with it).
2) The attribute should be used as [ViewToViewModel(MappingType = ...)]
3) The ViewToViewModel should handle the automatic mapping of property x on the view to property x on the view model. It will handle all change notifications automatically.

Binding Data to a UserControl hosted by a DataTemplate

I have a user control which exposes a property which is a long. I'd like to instantiate this control and bind to the exposed property in a data template.
I'm seeing xaml errors in the resource file. The ambiguous "must have derivative of panel as the root element". And when I run this in a debugger, I see that the value of TeamIdx is -1 and is not being set.
<DataTemplate x:Key="TeamScheduleTemplate">
<Grid HorizontalAlignment="Left" Width="400" Height="600">
<Team:ScheduleControl TeamIdx="{Binding Idx}" />
</Grid>
</DataTemplate>
public sealed partial class ScheduleControl : UserControl
{
public static readonly DependencyProperty TeamIdxProperty =
DependencyProperty.Register(
"TeamIdx",
typeof(long),
typeof(ScheduleControl),
new PropertyMetadata((long)-1));
public long TeamIdx
{
get { return (long)GetValue(TeamIdxProperty); }
set { SetValue(TeamIdxProperty, value); }
}
public ScheduleControl()
{
this.InitializeComponent();
var team = TeamLookup.GetTeam(TeamIdx);
}
}
Edit: It turns out that the binding doesn't happen until after the control is constructed. In retrospect, this makes total sense. The solution I used is below:
public sealed partial class ScheduleControl : UserControl
{
public static readonly DependencyProperty TeamIdxProperty =
DependencyProperty.Register(
"TeamIdx",
typeof(long),
typeof(ScheduleControl),
new PropertyMetadata(
(long)-1,
OnTeamIdxChanged));
public long TeamIdx
{
get { return (long)GetValue(TeamIdxProperty); }
set { SetValue(TeamIdxProperty, value); }
}
private static void OnTeamIdxChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var target = (ScheduleControl)sender;
target.OnTeamIdxChanged((long)e.NewValue);
}
private void OnTeamIdxChanged(long id)
{
var model = FindModel(id);
this.DataContext = model;
}
public ScheduleControl()
{
this.InitializeComponent();
}
}

How do I create a new instance of a control in the view without violating MVVM

I am encountering a similar problem to what is described in this SO question. The suggested solution is to create a new WebBrowser Control for each now page (PDF) we wish to present (Overwriting the old WebBrowser control).
What is the correct way of creating a new control like that in MVVM? I trying to keep the VM ignorant about the implementation of the view.
Why does the VM need to know? Why can't the view just hook into an appropriate event (define one if you like, or just use the PropertyChanged) and recreate the control?
Create an interface in the ViewModel named IBrowserCreator, with a method called CreateBrowser().
Create a static class in the ViewModel named ViewHelper, and add to it a static property of type IBrowserCreator named BrowserCreator.
In the View layer, create a new class called BrowserCreator, which implements ViewModel.IBrowserCreator.
In the View initialization code, instantiate a BrowserCreator, and assign it to ViewModel.ViewHelper.BrowserCreator.
From your ViewModel, you should now be able to call:
ViewHelper.BrowserCreator.CreateBrowser()
Obviously this answer is a framework only, but it should give you the general idea. You'll need to implement the CreateBrowser method to suit your exact needs.
why not simply use a Datatemplate and let WPF do the rest?
create a usercontrol with the webbrowser. you have to add an attached property because you can not bind to source directly.
<UserControl x:Class="WpfBrowser.BrowserControl"
xmlns:WpfBrowser="clr-namespace:WpfBrowser" >
<Grid>
<WebBrowser WpfBrowser:WebBrowserUtility.BindableSource="{Binding MyPdf}"/>
</Grid>
</UserControl>
create a viewmodel which handle your uri
public class MyPdfVM
{
public Uri MyPdf { get; set; }
public MyPdfVM()
{
this.MyPdf = new Uri(#"mypdf path");
}
}
take your pageviewmodel, add the pdfviewmodel and take a contentcontrol in your view
public class MyPageViewmodel: INotifyPropertyChanged
{
private MyPdfVM _myPdfStuff;
public MyPdfVM MyPdfStuff
{
get { return _myPdfStuff; }
set { _myPdfStuff = value; this.NotifyPropertyChanged(()=>this.MyPdfStuff);}
}
public MyViewmodel()
{
this.MyPdfStuff = new MyPdfVM();
}
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged<T>(Expression<Func<T>> property)
{
var propertyInfo = ((MemberExpression)property.Body).Member as PropertyInfo;
if (propertyInfo == null)
{
throw new ArgumentException("The lambda expression 'property' should point to a valid Property");
}
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyInfo.Name));
}
}
window.xaml
<Window x:Class="WpfBrowser.MainWindow"
xmlns:WpfBrowser="clr-namespace:WpfBrowser"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate DataType="{x:Type WpfBrowser:MyPdfVM}">
<WpfBrowser:BrowserControl />
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="64*" />
<RowDefinition Height="247*" />
</Grid.RowDefinitions>
<Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="32,14,0,0" Name="button1" VerticalAlignment="Top" Width="75" Click="button1_Click" />
<ContentControl Grid.Row="1" Content="{Binding MyPdfStuff}"/>
</Grid>
</Window>
window.xaml.cs
public partial class MainWindow : Window
{
private MyViewmodel _data;
public MainWindow()
{
_data = new MyViewmodel();
InitializeComponent();
this.DataContext = _data;
}
private void button1_Click(object sender, RoutedEventArgs e)
{
this._data.MyPdfStuff = new MyPdfVM() { MyPdf = new Uri(#"your other pdf path for testing") };
}
}
when ever you change the MyPdfStuff Property the webbroswer update the pdf.
attached property
public static class WebBrowserUtility
{
public static readonly DependencyProperty BindableSourceProperty =
DependencyProperty.RegisterAttached("BindableSource", typeof(string), typeof(WebBrowserUtility), new UIPropertyMetadata(null, BindableSourcePropertyChanged));
public static string GetBindableSource(DependencyObject obj)
{
return (string)obj.GetValue(BindableSourceProperty);
}
public static void SetBindableSource(DependencyObject obj, string value)
{
obj.SetValue(BindableSourceProperty, value);
}
public static void BindableSourcePropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
WebBrowser browser = o as WebBrowser;
if (browser != null)
{
string uri = e.NewValue as string;
browser.Source = string.IsNullOrWhiteSpace(uri) ? null:new Uri(uri);
}
}
}
EDIT: added some code so you can see that if you chane the PDFViewmodel your browsercontrol show the new pdf.

Create composite DataContext in XAML for a Usercontrol

I am trying to create a composite DataContext for a UserControl. Basically I have a control which has Order and Package properties and I wanted to create the composite object representing this datasource in XAML rather than in code.
This is how I am trying to display the UserControl (and create the DataContext):
<views:PackageDetailsControl>
<views:PackageDetailsControl.DataContext>
<vm:OrderPackagePair Package="{Binding Package, Mode=OneWay}"
Order="{Binding Order, Mode=OneWay}"/>
</views:PackageDetailsControl.DataContext>
</views:PackageDetailsControl>
The OrderPackagePair object is a simple dependency object that is created in XAML :
public class OrderPackagePair : DependencyObject
{
public OrderDetails Order
{
get { return (OrderDetails)GetValue(OrderProperty); }
set { SetValue(OrderProperty, value); }
}
public static readonly DependencyProperty OrderProperty =
DependencyProperty.Register("Order", typeof(OrderDetails), typeof(OrderPackagePair), new UIPropertyMetadata(null));
public PackageInfo Package
{
get { return (PackageInfo)GetValue(PackageProperty); }
set { SetValue(PackageProperty, value); }
}
public static readonly DependencyProperty PackageProperty =
DependencyProperty.Register("Package", typeof(PackageInfo), typeof(OrderPackagePair), new UIPropertyMetadata(null));
}
Order and Package are not bound correctly and are just null.
Yes I know there's probably a better way of doing this - but I cannot understand why this isn't working. Occasionally in Blend it'll work and then go blank again.
This will not work because DependencyObject(OrderPackagePair class) doesn't monitor internal changes of its dependency properties. As OrderPackagePair object remains the same, DataContext considered as unchanged.
On the opposite site, class Freezable is intented to notify subscribers that instance was changed when one of its dependency properties changed.
So, try to declare Freezable instead of DependencyObject as base class of OrderPackagePair.
------------- UPDATE --------
Yes, it works. In order to prove it I've implemented simple example.
Code of OrderPackagePairClass:
public class OrderPackagePair : Freezable
{
public OrderDetails Order
{
get { return (OrderDetails)GetValue(OrderProperty); }
set { SetValue(OrderProperty, value); }
}
public static readonly DependencyProperty OrderProperty =
DependencyProperty.Register("Order", typeof(OrderDetails), typeof(OrderPackagePair), new UIPropertyMetadata(null));
public PackageInfo Package
{
get { return (PackageInfo)GetValue(PackageProperty); }
set { SetValue(PackageProperty, value); }
}
public static readonly DependencyProperty PackageProperty =
DependencyProperty.Register("Package", typeof(PackageInfo), typeof(OrderPackagePair), new UIPropertyMetadata(null));
protected override Freezable CreateInstanceCore()
{
throw new NotImplementedException();
}
}
XAML:
<Window x:Class="WindowTest.MainWindow"
xmlns:self="clr-namespace:WindowTest"
Name="RootControl">
<StackPanel Margin="10" DataContextChanged="StackPanel_DataContextChanged">
<StackPanel.DataContext>
<self:OrderPackagePair Package="{Binding Path=DataContext.PackageInfo, Mode=OneWay, ElementName=RootControl}"
Order="{Binding Path=DataContext.OrderDetails, Mode=OneWay, ElementName=RootControl}"/>
</StackPanel.DataContext>
<Button Margin="10" Content="Change Package" Click="Button_Click"/>
</StackPanel>
</Window>
And code behind:
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
}
private OrderDetails _orderDetails;
public OrderDetails OrderDetails
{
get
{
return this._orderDetails;
}
set
{
this._orderDetails = value;
this.OnPropertyChanged("OrderDetails");
}
}
private PackageInfo _packageInfo;
public PackageInfo PackageInfo
{
get
{
return this._packageInfo;
}
set
{
this._packageInfo = value;
this.OnPropertyChanged("PackageInfo");
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
this.PackageInfo = new PackageInfo(DateTime.Now.ToString());
}
private void StackPanel_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
Trace.WriteLine("StackPanel.DataContext changed");
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
var safeEvent = this.PropertyChanged;
if (safeEvent != null)
{
safeEvent(this, new PropertyChangedEventArgs(name));
}
}
}
When you click the button, model changes PackageInfo property (for simplicity model and view are implemented in the same class). Dependency property OrderPackagePair.Package reacts on new value and overwrites its value. Due to Freezable nature, OrderPackagePair notifies all subscribers that it was changed and handler StackPanel_DataContextChanged is called. If you get back to DependencyObject as base class of OrderPackagePair - handler will be never called.
So, I suppose your code doesn't work because of other mistakes. You should carefully work with DataContext. For example, you wrote:
<views:PackageDetailsControl>
<views:PackageDetailsControl.DataContext>
<vm:OrderPackagePair Package="{Binding Package, Mode=OneWay}"
Order="{Binding Order, Mode=OneWay}"/>
</views:PackageDetailsControl.DataContext>
</views:PackageDetailsControl>
and certainly this is one of the problems. Binding expression is oriented on current DataContext. But you set DataContext as OrderPackagePair instance. So you binded OrderPackagePair.Package to OrderPackagePair.Package (I suppose, that your goal is to bind OrderPackagePair.Package to Model.Package). And that's why nothing happened.
In my example in binding expression I explicitly tell to which DataContext I want to bind:
Package="{Binding Path=DataContext.PackageInfo, Mode=OneWay, ElementName=RootControl}"

WPF Dependency Property Question

I'm new to WPF, and I'm trying to do what I thought would be a simple task - display the value of a field in my business object as it changes during my program. I know how to "force" it to change by manually changing the TextBox.Text property in C#, but as I'm learning WPF I want to do it the "right" way, and that means databinding.
Question #1: As far as I understand it, my choice is to either use a DependencyProperty or implement INotifyPropertyChanged in my business object, right?
Question #2: Here is a generic version of my code in which I attempted to go the DependencyProperty route.
Markup:
Button x:Name="nextButton" Content="Click" Grid.Row="2" Click="nextButton_Click" />
TextBox x:Name="myTextBox" Grid.Row="1" Text="{Binding Source=myTest, Path=Name, UpdateSourceTrigger=PropertyChanged, NotifyOnSourceUpdated=True}"/>
Code-Behind:
namespace DependencyTest2
{
///
/// Interaction logic for MainWindow.xaml
///
public partial class MainWindow : Window
{
private int i;
private TestSphere myTest;
public MainWindow()
{
InitializeComponent();
i = 0;
myTest = new TestSphere();
}
private void nextButton_Click(object sender, RoutedEventArgs e)
{
switch (i)
{
case 0:
myTest.Name = "string1";
break;
case 1:
myTest.Name = "string2";
break;
case 2:
myTest.Name = "string3";
break;
}
i++;
}
}
class TestSphere : DependencyObject
{
public string Name
{
get { return (string)GetValue(NameProperty); }
set { SetValue(NameProperty, value); }
}
public static readonly DependencyProperty NameProperty =
DependencyProperty.Register("Name", typeof(string), typeof(TestSphere));
public TestSphere()
{
Name = "default";
}
}
}
When I run the program, nothing appears in text box, even though the bound property has a value - is there something else I need to do to alert the binding that the source value has changed? I thought that using a DependencyProperty as the source would take care of that, but then again, I'm a WPF rookie. Thanks!
Steve
Ok, I tried to implement INotifyPropertyChanged using a wrapper class I found on codeproject as follows:
class TestSphere : NotifyProperyChangedBase
{
private string _Name;
public string Name
{
get { return _Name; }
set
{
this.CheckPropertyChanged("Name", ref _Name, ref value);
}
}
public TestSphere()
{
Name = "default";
}
}
public abstract class NotifyProperyChangedBase : INotifyPropertyChanged
{
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region methods
protected bool CheckPropertyChanged(string propertyName, ref T oldValue, ref T newValue)
{
if (oldValue == null && newValue == null)
{
return false;
}
if ((oldValue == null && newValue != null) || !oldValue.Equals((T)newValue))
{
oldValue = newValue;
FirePropertyChanged(propertyName);
return true;
}
return false;
}
protected void FirePropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
Here is my new Markup, too:
Grid Name="myGrid">
Grid.RowDefinitions>
RowDefinition Height="30"/>
RowDefinition Height="30"/>
RowDefinition Height="30"/>
RowDefinition Height="*"/>
/Grid.RowDefinitions>
Label x:Name="myLabel" Grid.Row="0" Foreground="Black" />
Button x:Name="nextButton" Content="Click" Grid.Row="2" Click="nextButton_Click" />
TextBox x:Name="myTextBox" Grid.Row="1" Text="{Binding Path=myTest.Name}"/>
/Grid>
I also added the line myGrid.DataContext = myTest; to 'public MainWindow()' immediately after I instantiate myTest. When I step through the resulting program, the value of this.PropertyChanged always evaluates to null, so that the PropertyChanged even never fires. Sorry in advance for what must be a really noob question.
Steve
You should only need to implement INotifyPropertyChanged on the TestSphere class, not DependencyObject. As you update the value, call PropertyChanged(this, new PropertyChangedEventArgs("Name")).
Second, you need to set the DataContext for the window in your code-behind. Lets say you used this in your XAML for the root grid element:
<Grid Name="MainForm">
Then in your code-behind, you'd do this:
MainForm.DataContext = this;
Finally, change the myTest property to public, and the binding in your XAML should then only need to be
Text="{Binding Path=myTest.Name}"

Resources