11/28/2007On the project I'm working on at the moment, I have a class that stores items in an ObservableCollection. I needed to be able to expose in the same class different subsets of this collection, keeping them all in sync while allowing to add/remove/update elements in all the collections.
For example, imagine you would like to create a collection of people ObservableCollection<People> but also have collection for Males and Females and pass these collections as parameters to other methods that can manipulate these collections (Add/Remove/Update People in the Males collection) and see the original collection updated.
My first approach was to create an ObservableCollection for each subset and use the CollectionChanged event to maintain all the lists in sync. While it worked, it was very code heavy. So today I wrote a simple FilteredObservableCollection class. It encapsulates an ObservableCollection but only enumerates the items that comply with the filter. It's similar to the CollectionView but it still allows to Add/Remove/Update elements in the collection.
The class is not derived from an ObservableCollection since it needs to be able to attach to an existing collection but it expose all the interfaces and members for the observable collection:
public
class
FilteredObservableCollection<T> : IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable, INotifyCollectionChanged, INotifyPropertyChanged { private
ObservableCollection<T> _collection; private
Predicate<T> _filter; private
event
NotifyCollectionChangedEventHandler _collectionchanged; private
event
PropertyChangedEventHandler _propertychanged;
public FilteredObservableCollection(ObservableCollection<T> collection) { _filter = null; _collection = collection; _collection.CollectionChanged += new
NotifyCollectionChangedEventHandler(OnCollectionChanged); ((INotifyPropertyChanged)_collection).PropertyChanged += new
PropertyChangedEventHandler(OnPropertyChanged); }
The two interesting bit in the Filtered collection are the handling on the CollectionChanged event and the enumerator:
The CollectionChanged event received from the encapsulated collection may not necessarily need to be passed by the filtered collection. An item added to or deleted from the main collection, if filtered out, should not generate an event for the filtered collection while a Change to an existing item may cause the item to appear in the list (it now complies with the filter) or disappear from the list (it no longer complies with the filter).
private
void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { if (_collectionchanged != null) { // Check the NewItems List<T> newlist = new
List<T>(); if (e.NewItems != null) foreach (T item in e.NewItems) if (_filter(item) == true) newlist.Add(item);
// Check the OldItems List<T> oldlist = new
List<T>(); if (e.OldItems != null) foreach (T item in e.OldItems) if (_filter(item) == true) oldlist.Add(item);
// Create the Add/Remove/Replace lists List<T> addlist = new
List<T>(); List<T> removelist = new
List<T>(); List<T> replacelist = new
List<T>();
// Fill the Add/Remove/Replace lists foreach (T item in newlist) if (oldlist.Contains(item)) replacelist.Add(item); else addlist.Add(item); foreach (T item in oldlist) if (newlist.Contains(item)) continue; else removelist.Add(item);
// Send the corrected event switch (e.Action) { case
NotifyCollectionChangedAction.Add: case
NotifyCollectionChangedAction.Move: case
NotifyCollectionChangedAction.Remove: case
NotifyCollectionChangedAction.Replace: if (addlist.Count > 0) _collectionchanged(this, new
NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, addlist)); if (replacelist.Count > 0) _collectionchanged(this, new
NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, replacelist)); if (removelist.Count > 0) _collectionchanged(this, new
NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removelist)); break; case
NotifyCollectionChangedAction.Reset: _collectionchanged(this, new
NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); break; } } }
The enumerator starts from the first compliant element in the collection and each call to Next() makes the cursor move to the next valid filtered item.
private
class
FilteredEnumerator : IEnumerator<T>, IEnumerator { private
FilteredObservableCollection<T> _filteredcollection; private
IEnumerator<T> _enumerator;
public FilteredEnumerator(FilteredObservableCollection<T> filteredcollection, IEnumerator<T> enumerator) { _filteredcollection = filteredcollection; _enumerator = enumerator; }
public T Current { get { if (_filteredcollection.Filter == null) return _enumerator.Current; else
if (_filteredcollection.Filter(_enumerator.Current) == false) throw
new
InvalidOperationException(); else return _enumerator.Current; } }
public
void Dispose() { _enumerator.Dispose(); }
object
IEnumerator.Current { get { return
this.Current; } }
public
bool MoveNext() { while (true) { if (_enumerator.MoveNext() == false) return
false; if (_filteredcollection.Filter == null || _filteredcollection.Filter(_enumerator.Current) == true) return
true; } }
public
void Reset() { _enumerator.Reset(); } } }
It's worked well so far but a lot remains to be done. I need to optimize the code to avoid for example having to calculate the Count at every call. I need to test all the possible collection manipulation and see if the CollectionChanged event is processed correctly. I need to implement the missing Move() method. I need to test multi-threading.
I've made the code available for download as part of a sample project that displays a list of integers and two subsets of multiples of 2 and of 3.
public
class
Data : DependencyObject { public
static
readonly
DependencyProperty FullListProperty = DependencyProperty.Register("FullList", typeof(ObservableCollection<int>), typeof(Data)); public
static
readonly
DependencyProperty List_2Property = DependencyProperty.Register("List_2", typeof(FilteredObservableCollection<int>), typeof(Data)); public
static
readonly
DependencyProperty List_3Property = DependencyProperty.Register("List_3", typeof(FilteredObservableCollection<int>), typeof(Data));
public
ObservableCollection<int> FullList { get { return (ObservableCollection<int>)GetValue(FullListProperty); } set { SetValue(FullListProperty, value); } }
public
FilteredObservableCollection<int> List_2 { get { return (FilteredObservableCollection<int>)GetValue(List_2Property); } set { SetValue(List_2Property, value); } }
public
FilteredObservableCollection<int> List_3 { get { return (FilteredObservableCollection<int>)GetValue(List_3Property); } set { SetValue(List_3Property, value); } }
public Data() { FullList = new
ObservableCollection<int>(); List_2 = new
FilteredObservableCollection<int>(FullList); List_3 = new
FilteredObservableCollection<int>(FullList);
List_2.Filter = new
Predicate<int>(Filter_2); List_3.Filter = new
Predicate<int>(Filter_3); }
private
bool Filter_2(int x) { return x % 2 == 0; } private
bool Filter_3(int x) { return x % 3 == 0; } }
I've used a DependencyObject and DependencyProperties but it wasn't necessary to use the FilteredObservableCollection.
Download the project source code here: Download 3/12/2007
You'll all be familiar with normal aggregation.
For example I can get the number of subcategories per category in AdventureWorks:
SELECT c.Name, COUNT(*) as SubCategoryCount FROM Production.ProductCategory c JOIN Production.ProductSubCategory s ON c.ProductCategoryID = s.ProductCategoryID GROUP BY c.Name
Which returns:
|
Name |
SubCategoryCount |
|
Accessories |
12 |
|
Bikes |
3 |
|
Clothing |
8 |
|
Components |
14 |
SQL 2005 supports 13 aggregation functions: AVG, CHECKSUM, CHECKSUM_AGG, COUNT, COUNT_BIG, GROUPING, MAX, MIN, SUM, STDEV, STDEVP, VAR and VARP but nothing to aggregate strings. You can create custom CLR aggregation functions but on some occasions, a simple SQL recursive query may be able to do what you need.
Let's have a look first at a case that doesn't require recursive queries. Let's create a comma delimited string of all the available categories. The data comes from the Product.ProductCategory table:
select * from Production.ProductCategory
And returns
|
ProductCategoryID |
Name |
rowguid |
ModifiedDate |
|
1 |
Bikes |
CFBDA25C-DF71-47A7-B81B-64EE161AA37C |
1998-06-01 00:00:00.000 |
|
2 |
Components |
C657828D-D808-4ABA-91A3-AF2CE02300E9 |
1998-06-01 00:00:00.000 |
|
3 |
Clothing |
10A7C342-CA82-48D4-8A38-46A2EB089B74 |
1998-06-01 00:00:00.000 |
|
4 |
Accessories |
2BE3BE36-D9A2-4EEE-B593-ED895D97C2A6 |
1998-06-01 00:00:00.000 |
To get the comma delimited list of Categories, we can use the COALESCE function and a string variable:
DECLARE @categories varchar(200) SET @categories = NULL
SELECT @categories = COALESCE(@categories + ',','') + Name FROM Production.ProductCategory
SELECT @categories
And returns, as expected a string "Accessories,Bikes,Clothing,Components"
But now, how would we return the recordset of categories with a second column containing the list of subcategories.
I can get the list of subcategories for a single category but not for each category using the COALESCE method.
This is where recursive queries help. First I need to number each subcategory within the category. I can do that using RANK and ROW_NUMBER functions.
SELECT c.Name as Category, s.Name as SubCategory, ROW_NUMBER() OVER(ORDER BY c.Name,s.Name) + 1 - RANK() OVER(ORDER BY c.Name) as SubCategoryNumber FROM Production.ProductCategory c JOIN Production.ProductSubCategory s ON c.ProductCategoryID = s.ProductCategoryID
And now the recursive query where each loop of the query adds to the comma delimited string with the initial value equal to the first subcategory
WITH
SubCategories(Category, Subcategory, SubCategoryNumber) AS (
SELECT c.Name as Category, s.Name as SubCategory, ROW_NUMBER() OVER(ORDER BY c.Name,s.Name) + 1 - RANK() OVER(ORDER BY c.Name) as SubCategoryNumber FROM Production.ProductCategory c JOIN Production.ProductSubCategory s ON c.ProductCategoryID = s.ProductCategoryID ),
TempCategories(Category, Subcategories, SubCategoryNumber) AS (
SELECT Category, CAST(Subcategory as varchar(max)), SubCategoryNumber FROM SubCategories WHERE SubCategoryNumber = 1
UNION ALL
SELECT TempCategories.Category, CAST(TempCategories.SubCategories + ',' + SubCategories.Subcategory as varchar(max)), TempCategories.SubCategoryNumber + 1 FROM TempCategories JOIN SubCategories ON TempCategories.Category = SubCategories.Category and TempCategories.SubCategoryNumber + 1 = SubCategories.SubCategoryNumber ),
MaxCategories(Category, SubCategoryNumber) AS (
SELECT Category, MAX(SubCategoryNumber) AS SubCategoryNumber FROM SubCategories GROUP BY Category ),
Categories (Category, Subcategories) AS (
SELECT TempCategories.Category, TempCategories.Subcategories FROM TempCategories JOIN MaxCategories ON TempCategories.Category = MaxCategories.Category AND TempCategories.SubCategoryNumber = MaxCategories.SubCategoryNumber )
SELECT * FROM Categories
Which returns:
|
Category |
Subcategories |
|
Components |
Bottom Brackets,Brakes,Chains,Cranksets,Derailleurs,Forks,Handlebars,Headsets,Mountain Frames,Pedals,Road Frames,Saddles,Touring Frames,Wheels |
|
Clothing |
Bib-Shorts,Caps,Gloves,Jerseys,Shorts,Socks,Tights,Vests |
|
Bikes |
Mountain Bikes,Road Bikes,Touring Bikes |
|
Accessories |
Bike Racks,Bike Stands,Bottles and Cages,Cleaners,Fenders,Helmets,Hydration Packs,Lights,Locks,Panniers,Pumps,Tires and Tubes |
If we look at each query:
SubCategories, we've seen already, returns the full list of categories and subcategories with a subcategory number, string at 1, for each category.
TempCategories is the recursive query and adds at each level the previous subcategory list with the current subcategory, so the first subcategory contains itself, the second catains the first and itself, the third contains the first, the second and itself and so on. We then need to only keep the last record of each category that contains every subcategory.
MaxCategories contains the highest SubcategoryNumber for each category.
Categories joins TempCategories and MaxCategory to only retain the highest subcategory number per category. The result we need.
The aggregation operation defined in TempCategories (here adding the string together) could be replaced with any custom aggregation. It iteratively, in a known sequence, combines the previous calculated value with the new value for that row.
2/9/2007Here's a simple step by step introduction to SSIS and how to import a flat file into SQL. This blog is based on the SQL 2005 SSIS Tutorial Lesson 1.
For this tutorial you need a file called SampleCurrencyData.txt that contains this information:
1.00010001 ARS 9/3/2001 0:00 0.99960016 1.00010001 ARS 9/4/2001 0:00 1.001001001 1.00020004 ARS 9/5/2001 0:00 0.99990001 1.00020004 ARS 9/6/2001 0:00 1.00040016 1.00050025 ARS 9/7/2001 0:00 0.99990001 1.00050025 ARS 9/8/2001 0:00 1.001001001 1.00050025 ARS 9/9/2001 0:00 1 1.00010001 ARS 9/10/2001 0:00 1.00040016 1.00020004 ARS 9/11/2001 0:00 0.99990001 1.00020004 ARS 9/12/2001 0:00 1.001101211
First open Visual Studio and create a new "Business Intelligence – Integration Services" project:
You'll get a blank SIS project:
Right click on the Connection Managers area and select "Add New Flat File Connection":
Give it a name, browse to the flat file. Change the Locale to English (United States). This is important if your default local doesn't match the file format. Change the format to Ragged right.
Next we need to adjust the column formats. Click on the Columns entry on the left of the dialog.
Then go to the Advanced section to change the column types: Click "Suggest Types", and press Ok with the proposed defaults.
You get:
Column 0: float Column 1: string (DT_STR) Column 2: date (DT_DATE) Column 3: float.
Let's adjust the type and column names to:
AverageRate: float CurrencyID: string (DT_WSTR) CurrencyDate: date (DT_DBTIMESTAMP) EndOfDayRate: float
Ok, press OK and the Flat File connection is defined.
Next we need to define the SQL Connection. Right lick the Connection Managers area again and select "New OLE DB Connection"
Press "New" In the Connection Manager dialog, set the parameters to connect to the AdventureWorksDW database, installed as part of the samples with SQL 2005.
And you get this:
Ok, press OK. We're done with the connections; let's start defining the control flow.
Back in the Package view, open the toolbox and drag a Data Flow Task to the Control Flow panel. Rename it (using right-click) to "Extract Sample Currency Data" or something meaningful.
Click on the dataflow tab and drag in 4 objects from the toolbox:
- One "Flat File Source" from the "Data Flow Sources"
- Two "Lookup" from the "Data Flow Transformations"
- One "OLE DB Destination" from the "Data Flow Destinations"
Rename the four objects, select the Flat file data source and drag the green arrow on to the first "Lookup". Select the first "Lookup" and drag the green arrow on to the second "Lookup". Select the second "Lookup" and drag the green arrow on to the "OLE DB Destination". It should look something like this:
Now let's configure the four objects:
Double click on the data source, select the flat file data source created before from the dropdown and press OK.
Double click the first lookup, ensure the OLEDB connection to AdventureWorksDW is selected, and select the DimCurrency table.
Go to "Columns", drag "CurrencyID" on to "CurrencyAlternateKey", and select the checkbox for CurrencyKey.
Press Ok, double click the second lookup, ensure AdventureWorksDW is selected, and select the DimTime table:
Select "Columns", drag the "CurrencyDate" column on to the "FullDateAlternateKey" column and select the checkbox for TimeKey:
Press Ok, and finally double click the OLE DB Destination object, ensure AdventureWorksDW is selected as the connection, select "FactCurrencyRate" as the destination table.
Click "Mappings" and check that the mapping looks correct:
Press OK and you're ready to run the package.
Select Debug->Run in the menu bar, the package will run and all dataflow tasks should turn green:
Press Stop to stop the debugger
You can use SQL Management studio to verify that the data from the file has been added to the FactCurrencyRate table.
A note: you may get this error:
With these details:
[Lookup Date [24]] Error: Row yielded no match during lookup. [Lookup Date [24]] Error: SSIS Error Code DTS_E_INDUCEDTRANSFORMFAILUREONERROR. The "component "Lookup Date" (24)" failed because error code 0xC020901E occurred, and the error row disposition on "output "Lookup Output" (26)" specifies failure on error. An error occurred on the specified object of the specified component. There may be error messages posted before this with more information about the failure. [DTS.Pipeline] Error: SSIS Error Code DTS_E_PROCESSINPUTFAILED. The ProcessInput method on component "Lookup Date" (24) failed with error code 0xC0209029. The identified component returned an error from the ProcessInput method. The error is specific to the component, but the error is fatal and will cause the Data Flow task to stop running. There may be error messages posted before this with more information about the failure. [DTS.Pipeline] Error: SSIS Error Code DTS_E_THREADFAILED. Thread "WorkThread0" has exited with error code 0xC0209029. There may be error messages posted before this with more information on why the thread has exited.
This is caused if you missed a step in the instructions above regarding the Locale of the data. If you are running this tutorial on a computer where English(United States) is not the default locale , the Flat File Data Source will by default select your locale and this will not match the locale of the data in the file. This image for example shows an invalid locale for the data:
If I switch back to English (United States) and run the test again, the error goes away.
For more information, go to http://msdn2.microsoft.com/en-us/library/ms169917.aspx and look at Lesson 1.
1/30/2007I spent the Australia Day long week-end build a hutch for the new addition to the family, as yet unnamed, Bunny1 and Bunny2. The hutch has two individual sleeping quarters, each with a private bedroom and 'dining' room and an exercise yard on the ground floor. Here's a few pictures of the completed hutch and the bunnies:
'Gray Bunny'
'Black Bunny'
Madeleine and the hutch
Three days of hard labour
 In my previous blog on DependencyProperty, I talked about how to create properties in objects using DependencyProperty and DependencyObject, to have support for Databinding, defaults, expressions, events and validations. But on some occasions, you want to assign a parent object needs to assign a property to the children objects, regardless of the type of object.
Traditionally, this has been done using base class and inheritance. For example, if you wanted to create a panel control that understands commands, you would need to create a base class 'CmdControl" something like this:
public
partial
class
CmdControl : UserControl { public CmdControl() { InitializeComponent(); }
private
string _cmd;
public
string Cmd { get { return _cmd; } set { _cmd = value; } } }
Then you would create all your controls derived from CmdControl, thus ensuring that each control has a Cmd property:
public
partial
class
MyControl : CmdControl { public MyControl() { this.Cmd = "Do this";
InitializeComponent(); } }
But this method did not allow the use of existing controls or classes.
With attached properties, a property can be set for each child control but the storage of the value is handled by the parent class. Let's see how to declare the Grid control and the Cmd attached property:
public
partial
class
MyPanel : System.Windows.Controls.Grid { public MyPanel() { InitializeComponent(); }
public
static
string GetCmd(DependencyObject obj) { return (string)obj.GetValue(CmdProperty); }
public
static
void SetCmd(DependencyObject obj, string value) { obj.SetValue(CmdProperty, value); }
public
static
readonly DependencyProperty CmdProperty = DependencyProperty.RegisterAttached("Cmd", typeof(string), typeof(MyPanel), new UIPropertyMetadata("Open")); }
In XAML, the panel is used as per my previous blog:
<Window
x:Class="WPF2.Window1" xmlns=http://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml xmlns:AppCode="clr-namespace:WPF2" Title="WPF2"
Height="263"
Width="239" > <AppCode:MyPanel
x:Name="Panel1"> <Button
Name="Button1"
Height="23"
Margin="50,0,50,0" >Button1</Button> <Button
Name="Button2"
Height="23"
Margin="50,50,50,0" >Button2</Button> </AppCode:MyPanel> </Window>
You can then programmatically assign a command value to any child control:
public
partial
class
Window1 : System.Windows.Window { public Window1() { InitializeComponent();
MyPanel.SetCmd(Button1, "Open 1"); MyPanel.SetCmd(Button2, "Close 1");
Button1.Click += new
RoutedEventHandler(Button_Click); Button2.Click += new
RoutedEventHandler(Button_Click); }
void Button_Click(object sender, RoutedEventArgs e) { MessageBox.Show("My Command is " + MyPanel.GetCmd(sender as
DependencyObject)); } }
Or you can do the same thing in XAML:
<Window
x:Class="WPF2.Window1" xmlns=http://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml xmlns:AppCode="clr-namespace:WPF2" Title="WPF2"
Height="263"
Width="239" > <AppCode:MyPanel
x:Name="Panel1"> <Button
AppCode:MyPanel.Cmd="Open 1"
Click="Button_Click"
Height="23"
Margin="50,0,50,0" >Button1</Button> <Button
AppCode:MyPanel.Cmd="Close 1"
Click="Button_Click"
Height="23"
Margin="50,50,50,0" >Button2</Button> </AppCode:MyPanel> </Window>
When you launch the application, and you click on either button, you get a message box with the text of the command assigned to that button.
More information on attached properties can be found here.
1/12/2007Up until now, you would create properties in a class using this syntax:
public
class
MyObject { private
string MyPropertyValue;
public
string MyProperty { set { MyPropertyValue = value; } get { return MyPropertyValue; } } }
This is fine but then you have a lot of code to add to handle additional features such as PropertyChanged event, data binding, default value, validation.
DependencyObject and DependencyProperty provide a simpler way to implement properties and provide directly off the shelf:
- DataBinding
- Events
- Validation
- Default Value
As well as elements more specific to Windows Foundation and .NET 3.0:
- Animation
- Style
- Attached properties
- Expressions
So let's see how to use these new classes. First we create a class similar to the one above:
public
class
Lift : DependencyObject { public
static
DependencyProperty FloorProperty = DependencyProperty.Register("Floor", typeof(int), typeof(Lift));
public
int Floor { get { return (int)GetValue(FloorProperty); } set { SetValue(FloorProperty, value); } } }
I have a class Lift with a property called Floor. Already I can implement many of the WPF feature. The Lift class is ready for DataBinding, the Floor property will accept expressions to be set and can be used as part of another expression. Let's look at this simple XAML code:
<Window
x:Class="TestWPF1.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:AppCode="clr-namespace:TestWPF1" Title="TestWPF1"
Height="300"
Width="300" > <Window.Resources> <AppCode:Lift
x:Key="Lift1"
Floor="1" /> </Window.Resources> <StackPanel
Orientation="Horizontal"> <TextBlock
Text="{Binding Path=Floor}" /> <StackPanel.DataContext> <Binding
Source="{StaticResource Lift1}" /> </StackPanel.DataContext> </StackPanel> </Window>
I created an instance of Lift in XAML and databound it to the StackPanel.
Still I rely on the user to set the Floor value to a correct value. Why not assign a default value:
public
static
DependencyProperty FloorProperty = DependencyProperty.Register("Floor", typeof(int),typeof(Lift),new
PropertyMetadata(0));
By setting the default value in the metadata, I can now create an instance of the Lift class without specifying the value for Floor:
<AppCode:Lift
x:Key="Lift1" />
Now I'd like to display the kind of goods for sale on the floor of that building. I add a second property to my class called "Goods".
public
static
DependencyProperty GoodsProperty = DependencyProperty.Register("Goods", typeof(string), typeof(Lift));
public
string Goods { get { return (string)GetValue(GoodsProperty); } set { SetValue(GoodsProperty, value); } }
And I can use it directly in XAML:
<AppCode:Lift
x:Key="Lift1"
Floor="1"
Goods="Perfumes" /> … <TextBlock
Text="{Binding Path=Floor}" /> <TextBlock
Text=": " /> <TextBlock
Text="{Binding Path=Goods}" />
But I'd like set the goods value directly in my class according to the floor set. I can do that by handling the PropertyChanged event on the Floor property. I just expand a bit the property definition to get the Changed event:
public
static
DependencyProperty FloorProperty = DependencyProperty.Register("Floor", typeof(int),typeof(Lift),new
PropertyMetadata(0,new
PropertyChangedCallback(OnFloorChanged));
private
static
void OnFloorChanged(DependencyObject obj, DependencyPropertyChangedEventArgs arg) { Lift l = (Lift)obj; switch (l.Floor) { case 0: l.Goods = "Books"; break; case 1: l.Goods = "Perfumes"; break; case 2: l.Goods = "Toys"; break; case 3: l.Goods = "Kitchenware"; break; } }
So in XAML, it looks like:
<AppCode:Lift
x:Key="Lift1"
Floor="1" /> … <TextBlock
Text="{Binding Path=Floor}" /> <TextBlock
Text=": " /> <TextBlock
Text="{Binding Path=Goods}" />
I no longer have to supply the Goods value, it's set when the Floor value is modified.
Ok so we've looked at Databinding, Property Changed, Default value, now we need validation. My building doesn't have an infinite number of floors so I'd like to restrict the Floor value to between 0 and 3.
I can add validation by handling the Validate event:
public
static
DependencyProperty FloorProperty = DependencyProperty.Register(("Floor", typeof(int),typeof(Lift),new
PropertyMetadata(0,new
PropertyChangedCallback(OnFloorChanged)),new
ValidateValueCallback(OnFloorValidate));
private
static
bool OnFloorValidate(object obj) { return obj is
int && (int)obj >= 0 && (int)obj <= 3; }
This not only validates that the value is between 0 and 3 but also checks that the value is actually an integer. An exception is thrown if you try to set the property to an incorrect value.
The final class looks like:
class
Lift : DependencyObject { public
static
DependencyProperty FloorProperty = DependencyProperty.Register(("Floor", typeof(int),typeof(Lift),new
PropertyMetadata(0,new
PropertyChangedCallback(OnFloorChanged)),new
ValidateValueCallback(OnFloorValidate));
private
static
void OnFloorChanged(DependencyObject obj, DependencyPropertyChangedEventArgs arg) { Lift l = (Lift)obj; switch (l.Floor) { case 0: l.Goods = "Books"; break; case 1: l.Goods = "Perfumes"; break; case 2: l.Goods = "Toys"; break; case 3: l.Goods = "Kitchenware"; break; } }
private
static
bool OnFloorValidate(object obj) { return obj is
int && (int)obj >= 0 && (int)obj <= 3; }
public
int Floor { get { return (int)GetValue(FloorProperty); } set { SetValue(FloorProperty, value); } }
public
static
DependencyProperty GoodsProperty = DependencyProperty.Register("Goods", typeof(string), typeof(Lift));
public
string Goods { get { return (string)GetValue(GoodsProperty); } set { SetValue(GoodsProperty, value); } } }
And the XAML:
<Window
x:Class="TestWPF1.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:AppCode="clr-namespace:TestWPF1" Title="TestWPF1"
Height="300"
Width="300" > <Window.Resources> <AppCode:Lift
x:Key="Lift1"
Floor="1" /> <AppCode:Lift
x:Key="Lift2"
Floor="3" /> </Window.Resources> <StackPanel
Orientation="Vertical"> <StackPanel
Orientation="Horizontal"> <TextBlock
Text="{Binding Path=Floor}" /> <TextBlock
Text=", " /> <TextBlock
Text="{Binding Path=Goods}" /> <StackPanel.DataContext> <Binding
Source="{StaticResource Lift1}" /> </StackPanel.DataContext> </StackPanel> <StackPanel
Orientation="Horizontal"> <TextBlock
Text="{Binding Path=Floor}" /> <TextBlock
Text=", " /> <TextBlock
Text="{Binding Path=Goods}" /> <StackPanel.DataContext> <Binding
Source="{StaticResource Lift2}" /> </StackPanel.DataContext> </StackPanel> </StackPanel> </Window>
1/8/2007
Born in France but living in Sydney, Australia for the past 20 years, I've had the opportunity to try many French patisseries around Sydney and in my opinion, you can't miss "Le Patissier" in Neutral Bay, just a minute walk from the Oaks, along Military road. The selection of cakes is very large and they are all delicious. You can also grab a few fresh chocolates, bread and croissants of course.
A few more to visit:
- La Renaissance in the Rocks, on Argyle street
- Patisson in St Yves, and also at Horsnby Westfield Shopping Centre
My wife, Lisa, has been collection Peanuts memorabilia for more than 25 years. The 'Snoopy' room contains 4700 Snoopies, collected from all other the world. You can read more about it on http://www.snoopyaustralia.com/
|
|
|
|
|