Now we starting getting into a more code-heavy portion of this series, thankfully though this means the groundwork is all set for the most part and after adding the modules we will have a complete application that can be provided with full source.
The Jobs module will have two concerns- adding and maintaining jobs that can then be broadcast out to the website. How they are displayed on the site will be handled by our admin system (which will just poll from this common database), so we aren't too concerned with that, but rather with getting the information into the system and allowing the backend administration/HR users to keep things up to date. Since there is a fair bit of information that we want to display, we're going to move editing to a separate view so we can get all that information in an easy-to-use spot. With all the files created for this module, the project looks something like this:
And now... on to the code.
XAML for the Job Posting View
All we really need for the Job Posting View is a RadGridView and a few buttons. This will let us both show off records and perform operations on the records without much hassle. That XAML is going to look something like this:
01.
<
Grid
x:Name
=
"LayoutRoot"
02.
Background
=
"White"
>
03.
<
Grid.RowDefinitions
>
04.
<
RowDefinition
Height
=
"30"
/>
05.
<
RowDefinition
/>
06.
</
Grid.RowDefinitions
>
07.
<
StackPanel
Orientation
=
"Horizontal"
>
08.
<
Button
x:Name
=
"xAddRecordButton"
09.
Content
=
"Add Job"
10.
Width
=
"120"
11.
cal:Click.Command
=
"{Binding AddRecord}"
12.
telerik:StyleManager.Theme
=
"Windows7"
/>
13.
<
Button
x:Name
=
"xEditRecordButton"
14.
Content
=
"Edit Job"
15.
Width
=
"120"
16.
cal:Click.Command
=
"{Binding EditRecord}"
17.
telerik:StyleManager.Theme
=
"Windows7"
/>
18.
</
StackPanel
>
19.
<
telerikGrid:RadGridView
x:Name
=
"xJobsGrid"
20.
Grid.Row
=
"1"
21.
IsReadOnly
=
"True"
22.
AutoGenerateColumns
=
"False"
23.
ColumnWidth
=
"*"
24.
RowDetailsVisibilityMode
=
"VisibleWhenSelected"
25.
ItemsSource
=
"{Binding MyJobs}"
26.
SelectedItem
=
"{Binding SelectedJob, Mode=TwoWay}"
27.
command:SelectedItemChangedEventClass.Command
=
"{Binding SelectedItemChanged}"
>
28.
<
telerikGrid:RadGridView.Columns
>
29.
<
telerikGrid:GridViewDataColumn
Header
=
"Job Title"
30.
DataMemberBinding
=
"{Binding JobTitle}"
31.
UniqueName
=
"JobTitle"
/>
32.
<
telerikGrid:GridViewDataColumn
Header
=
"Location"
33.
DataMemberBinding
=
"{Binding Location}"
34.
UniqueName
=
"Location"
/>
35.
<
telerikGrid:GridViewDataColumn
Header
=
"Resume Required"
36.
DataMemberBinding
=
"{Binding NeedsResume}"
37.
UniqueName
=
"NeedsResume"
/>
38.
<
telerikGrid:GridViewDataColumn
Header
=
"CV Required"
39.
DataMemberBinding
=
"{Binding NeedsCV}"
40.
UniqueName
=
"NeedsCV"
/>
41.
<
telerikGrid:GridViewDataColumn
Header
=
"Overview Required"
42.
DataMemberBinding
=
"{Binding NeedsOverview}"
43.
UniqueName
=
"NeedsOverview"
/>
44.
<
telerikGrid:GridViewDataColumn
Header
=
"Active"
45.
DataMemberBinding
=
"{Binding IsActive}"
46.
UniqueName
=
"IsActive"
/>
47.
</
telerikGrid:RadGridView.Columns
>
48.
</
telerikGrid:RadGridView
>
49.
</
Grid
>
I'll explain what's happening here by line numbers:
So those first three probably make sense to you as far as Silverlight/WPF binding magic is concerned, but for line 27... This actually comes from something I read on Damien Schenkelman's blog back in the day for creating an attached behavior from any event. So, any time you see me using command:Whatever.Command, the backing for it is actually something like this:
SelectedItemChangedEventBehavior.cs:
01.
public
class
SelectedItemChangedEventBehavior : CommandBehaviorBase<Telerik.Windows.Controls.DataControl>
02.
{
03.
public
SelectedItemChangedEventBehavior(DataControl element)
04.
:
base
(element)
05.
{
06.
element.SelectionChanged +=
new
EventHandler<SelectionChangeEventArgs>(element_SelectionChanged);
07.
}
08.
void
element_SelectionChanged(
object
sender, SelectionChangeEventArgs e)
09.
{
10.
// We'll only ever allow single selection, so will only need item index 0
11.
base
.CommandParameter = e.AddedItems[0];
12.
base
.ExecuteCommand();
13.
}
14.
}
SelectedItemChangedEventClass.cs:
01.
public
class
SelectedItemChangedEventClass
02.
{
03.
#region The Command Stuff
04.
public
static
ICommand GetCommand(DependencyObject obj)
05.
{
06.
return
(ICommand)obj.GetValue(CommandProperty);
07.
}
08.
public
static
void
SetCommand(DependencyObject obj, ICommand value)
09.
{
10.
obj.SetValue(CommandProperty, value);
11.
}
12.
public
static
readonly
DependencyProperty CommandProperty =
13.
DependencyProperty.RegisterAttached(
"Command"
,
typeof
(ICommand),
14.
typeof
(SelectedItemChangedEventClass),
new
PropertyMetadata(OnSetCommandCallback));
15.
public
static
void
OnSetCommandCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
16.
{
17.
DataControl element = dependencyObject
as
DataControl;
18.
if
(element !=
null
)
19.
{
20.
SelectedItemChangedEventBehavior behavior = GetOrCreateBehavior(element);
21.
behavior.Command = e.NewValue
as
ICommand;
22.
}
23.
}
24.
#endregion
25.
public
static
SelectedItemChangedEventBehavior GetOrCreateBehavior(DataControl element)
26.
{
27.
SelectedItemChangedEventBehavior behavior = element.GetValue(SelectedItemChangedEventBehaviorProperty)
as
SelectedItemChangedEventBehavior;
28.
if
(behavior ==
null
)
29.
{
30.
behavior =
new
SelectedItemChangedEventBehavior(element);
31.
element.SetValue(SelectedItemChangedEventBehaviorProperty, behavior);
32.
}
33.
return
behavior;
34.
}
35.
public
static
SelectedItemChangedEventBehavior GetSelectedItemChangedEventBehavior(DependencyObject obj)
36.
{
37.
return
(SelectedItemChangedEventBehavior)obj.GetValue(SelectedItemChangedEventBehaviorProperty);
38.
}
39.
public
static
void
SetSelectedItemChangedEventBehavior(DependencyObject obj, SelectedItemChangedEventBehavior value)
40.
{
41.
obj.SetValue(SelectedItemChangedEventBehaviorProperty, value);
42.
}
43.
public
static
readonly
DependencyProperty SelectedItemChangedEventBehaviorProperty =
44.
DependencyProperty.RegisterAttached(
"SelectedItemChangedEventBehavior"
,
45.
typeof
(SelectedItemChangedEventBehavior),
typeof
(SelectedItemChangedEventClass),
null
);
46.
}
These end up looking very similar from command to command, but in a nutshell you create a command based on any event, determine what the parameter for it will be, then execute. It attaches via XAML and ties to a DelegateCommand in the viewmodel, so you get the full event experience (since some controls get a bit event-rich for added functionality).
Simple enough, right?
Viewmodel for the Job Posting View
The Viewmodel is going to need to handle all events going back and forth, maintaining interactions with the data we are using, and both publishing and subscribing to events. Rather than breaking this into tons of little pieces, I'll give you a nice view of the entire viewmodel and then hit up the important points line-by-line:
001.
public
class
JobPostingViewModel : ViewModelBase
002.
{
003.
private
readonly
IEventAggregator eventAggregator;
004.
private
readonly
IRegionManager regionManager;
005.
public
DelegateCommand<
object
> AddRecord {
get
;
set
; }
006.
public
DelegateCommand<
object
> EditRecord {
get
;
set
; }
007.
public
DelegateCommand<
object
> SelectedItemChanged {
get
;
set
; }
008.
public
RecruitingContext context;
009.
private
QueryableCollectionView _myJobs;
010.
public
QueryableCollectionView MyJobs
011.
{
012.
get
{
return
_myJobs; }
013.
}
014.
private
QueryableCollectionView _selectionJobActionHistory;
015.
public
QueryableCollectionView SelectedJobActionHistory
016.
{
017.
get
{
return
_selectionJobActionHistory; }
018.
}
019.
private
JobPosting _selectedJob;
020.
public
JobPosting SelectedJob
021.
{
022.
get
{
return
_selectedJob; }
023.
set
024.
{
025.
if
(value != _selectedJob)
026.
{
027.
_selectedJob = value;
028.
NotifyChanged(
"SelectedJob"
);
029.
}
030.
}
031.
}
032.
public
SubscriptionToken editToken =
new
SubscriptionToken();
033.
public
SubscriptionToken addToken =
new
SubscriptionToken();
034.
public
JobPostingViewModel(IEventAggregator eventAgg, IRegionManager regionmanager)
035.
{
036.
// set Unity items
037.
this
.eventAggregator = eventAgg;
038.
this
.regionManager = regionmanager;
039.
// load our context
040.
context =
new
RecruitingContext();
041.
this
._myJobs =
new
QueryableCollectionView(context.JobPostings);
042.
context.Load(context.GetJobPostingsQuery());
043.
// set command events
044.
this
.AddRecord =
new
DelegateCommand<
object
>(
this
.AddNewRecord);
045.
this
.EditRecord =
new
DelegateCommand<
object
>(
this
.EditExistingRecord);
046.
this
.SelectedItemChanged =
new
DelegateCommand<
object
>(
this
.SelectedRecordChanged);
047.
SetSubscriptions();
048.
}
049.
#region DelegateCommands from View
050.
public
void
AddNewRecord(
object
obj)
051.
{
052.
this
.eventAggregator.GetEvent<AddJobEvent>().Publish(
true
);
053.
}
054.
public
void
EditExistingRecord(
object
obj)
055.
{
056.
if
(_selectedJob ==
null
)
057.
{
058.
this
.eventAggregator.GetEvent<NotifyUserEvent>().Publish(
"No job selected."
);
059.
}
060.
else
061.
{
062.
this
._myJobs.EditItem(
this
._selectedJob);
063.
this
.eventAggregator.GetEvent<EditJobEvent>().Publish(
this
._selectedJob);
064.
}
065.
}
066.
public
void
SelectedRecordChanged(
object
obj)
067.
{
068.
if
(obj.GetType() ==
typeof
(ActionHistory))
069.
{
070.
// event bubbles up so we don't catch items from the ActionHistory grid
071.
}
072.
else
073.
{
074.
JobPosting job = obj
as
JobPosting;
075.
GrabHistory(job.PostingID);
076.
}
077.
}
078.
#endregion
079.
#region Subscription Declaration and Events
080.
public
void
SetSubscriptions()
081.
{
082.
EditJobCompleteEvent editComplete = eventAggregator.GetEvent<EditJobCompleteEvent>();
083.
if
(editToken !=
null
)
084.
editComplete.Unsubscribe(editToken);
085.
editToken = editComplete.Subscribe(
this
.EditCompleteEventHandler);
086.
AddJobCompleteEvent addComplete = eventAggregator.GetEvent<AddJobCompleteEvent>();
087.
if
(addToken !=
null
)
088.
addComplete.Unsubscribe(addToken);
089.
addToken = addComplete.Subscribe(
this
.AddCompleteEventHandler);
090.
}
091.
public
void
EditCompleteEventHandler(
bool
complete)
092.
{
093.
if
(complete)
094.
{
095.
JobPosting thisJob = _myJobs.CurrentEditItem
as
JobPosting;
096.
this
._myJobs.CommitEdit();
097.
this
.context.SubmitChanges((s) =>
098.
{
099.
ActionHistory myAction =
new
ActionHistory();
100.
myAction.PostingID = thisJob.PostingID;
101.
myAction.Description = String.Format(
"Job '{0}' has been edited by {1}"
, thisJob.JobTitle,
"default user"
);
102.
myAction.TimeStamp = DateTime.Now;
103.
eventAggregator.GetEvent<AddActionEvent>().Publish(myAction);
104.
}
105.
,
null
);
106.
}
107.
else
108.
{
109.
this
._myJobs.CancelEdit();
110.
}
111.
this
.MakeMeActive(
this
.regionManager,
"MainRegion"
,
"JobPostingsView"
);
112.
}
113.
public
void
AddCompleteEventHandler(JobPosting job)
114.
{
115.
if
(job ==
null
)
116.
{
117.
// do nothing, new job add cancelled
118.
}
119.
else
120.
{
121.
this
.context.JobPostings.Add(job);
122.
this
.context.SubmitChanges((s) =>
123.
{
124.
ActionHistory myAction =
new
ActionHistory();
125.
myAction.PostingID = job.PostingID;
126.
myAction.Description = String.Format(
"Job '{0}' has been added by {1}"
, job.JobTitle,
"default user"
);
127.
myAction.TimeStamp = DateTime.Now;
128.
eventAggregator.GetEvent<AddActionEvent>().Publish(myAction);
129.
}
130.
,
null
);
131.
}
132.
this
.MakeMeActive(
this
.regionManager,
"MainRegion"
,
"JobPostingsView"
);
133.
}
134.
#endregion
135.
public
void
GrabHistory(
int
postID)
136.
{
137.
context.ActionHistories.Clear();
138.
_selectionJobActionHistory =
new
QueryableCollectionView(context.ActionHistories);
139.
context.Load(context.GetHistoryForJobQuery(postID));
140.
}
Taking it from the top, we're injecting an Event Aggregator and Region Manager for use down the road and also have the public DelegateCommands (just like in the Menu module). We also grab a reference to our context, which we'll obviously need for data, then set up a few fields with public properties tied to them. We're also setting subscription tokens, which we have not yet seen but I will get into below.
The AddNewRecord (50) and EditExistingRecord (54) methods should speak for themselves for functionality, the one thing of note is we're sending events off to the Event Aggregator which some module, somewhere will take care of. Since these aren't entirely relying on one another, the Jobs View doesn't care if anyone is listening, but it will publish AddJobEvent (52), NotifyUserEvent (58) and EditJobEvent (63) regardless. Don't mind the GrabHistory() method so much, that is just grabbing history items (visibly being created in the SubmitChanges callbacks), and adding them to the database. Every action will trigger a history event, so we'll know who modified what and when, just in case. ;)
So where are we at? Well, if we click to Add a job, we publish an event, if we edit a job, we publish an event with the selected record (attained through the magic of binding). Where is this all going though? To the Viewmodel, of course!
XAML for the AddEditJobView
This is pretty straightforward except for one thing, noted below:
001.
<
Grid
x:Name
=
"LayoutRoot"
002.
Background
=
"White"
>
003.
<
Grid
x:Name
=
"xEditGrid"
004.
Margin
=
"10"
005.
validationHelper:ValidationScope.Errors
=
"{Binding Errors}"
>
006.
<
Grid.Background
>
007.
<
LinearGradientBrush
EndPoint
=
"0.5,1"
008.
StartPoint
=
"0.5,0"
>
009.
<
GradientStop
Color
=
"#FFC7C7C7"
010.
Offset
=
"0"
/>
011.
<
GradientStop
Color
=
"#FFF6F3F3"
012.
Offset
=
"1"
/>
013.
</
LinearGradientBrush
>
014.
</
Grid.Background
>
015.
<
Grid.RowDefinitions
>
016.
<
RowDefinition
Height
=
"40"
/>
017.
<
RowDefinition
Height
=
"40"
/>
018.
<
RowDefinition
Height
=
"40"
/>
019.
<
RowDefinition
Height
=
"100"
/>
020.
<
RowDefinition
Height
=
"100"
/>
021.
<
RowDefinition
Height
=
"100"
/>
022.
<
RowDefinition
Height
=
"40"
/>
023.
<
RowDefinition
Height
=
"40"
/>
024.
<
RowDefinition
Height
=
"40"
/>
025.
</
Grid.RowDefinitions
>
026.
<
Grid.ColumnDefinitions
>
027.
<
ColumnDefinition
Width
=
"150"
/>
028.
<
ColumnDefinition
Width
=
"150"
/>
029.
<
ColumnDefinition
Width
=
"300"
/>
030.
<
ColumnDefinition
Width
=
"100"
/>
031.
</
Grid.ColumnDefinitions
>
032.
<!-- Title -->
033.
<
TextBlock
Margin
=
"8"
034.
Text
=
"{Binding AddEditString}"
035.
TextWrapping
=
"Wrap"
036.
Grid.Column
=
"1"
037.
Grid.ColumnSpan
=
"2"
038.
FontSize
=
"16"
/>
039.
<!-- Data entry area-->
040.
041.
<
TextBlock
Margin
=
"8,0,0,0"
042.
Style
=
"{StaticResource LabelTxb}"
043.
Grid.Row
=
"1"
044.
Text
=
"Job Title"
045.
VerticalAlignment
=
"Center"
/>
046.
<
TextBox
x:Name
=
"xJobTitleTB"
047.
Margin
=
"0,8"
048.
Grid.Column
=
"1"
049.
Grid.Row
=
"1"
050.
Text
=
"{Binding activeJob.JobTitle, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnExceptions=True}"
051.
Grid.ColumnSpan
=
"2"
/>
052.
<
TextBlock
Margin
=
"8,0,0,0"
053.
Grid.Row
=
"2"
054.
Text
=
"Location"
055.
d:LayoutOverrides
=
"Height"
056.
VerticalAlignment
=
"Center"
/>
057.
<
TextBox
x:Name
=
"xLocationTB"
058.
Margin
=
"0,8"
059.
Grid.Column
=
"1"
060.
Grid.Row
=
"2"
061.
Text
=
"{Binding activeJob.Location, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnExceptions=True}"
062.
Grid.ColumnSpan
=
"2"
/>
063.
064.
<
TextBlock
Margin
=
"8,11,8,0"
065.
Grid.Row
=
"3"
066.
Text
=
"Description"
067.
TextWrapping
=
"Wrap"
068.
VerticalAlignment
=
"Top"
/>
069.
070.
<
TextBox
x:Name
=
"xDescriptionTB"
071.
Height
=
"84"
072.
TextWrapping
=
"Wrap"
073.
ScrollViewer.VerticalScrollBarVisibility
=
"Auto"
074.
Grid.Column
=
"1"
075.
Grid.Row
=
"3"
076.
Text
=
"{Binding activeJob.Description, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnExceptions=True}"
077.
Grid.ColumnSpan
=
"2"
/>
078.
<
TextBlock
Margin
=
"8,11,8,0"
079.
Grid.Row
=
"4"
080.
Text
=
"Requirements"
081.
TextWrapping
=
"Wrap"
082.
VerticalAlignment
=
"Top"
/>
083.
084.
<
TextBox
x:Name
=
"xRequirementsTB"
085.
Height
=
"84"
086.
TextWrapping
=
"Wrap"
087.
ScrollViewer.VerticalScrollBarVisibility
=
"Auto"
088.
Grid.Column
=
"1"
089.
Grid.Row
=
"4"
090.
Text
=
"{Binding activeJob.Requirements, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnExceptions=True}"
091.
Grid.ColumnSpan
=
"2"
/>
092.
<
TextBlock
Margin
=
"8,11,8,0"
093.
Grid.Row
=
"5"
094.
Text
=
"Qualifications"
095.
TextWrapping
=
"Wrap"
096.
VerticalAlignment
=
"Top"
/>
097.
098.
<
TextBox
x:Name
=
"xQualificationsTB"
099.
Height
=
"84"
100.
TextWrapping
=
"Wrap"
101.
ScrollViewer.VerticalScrollBarVisibility
=
"Auto"
102.
Grid.Column
=
"1"
103.
Grid.Row
=
"5"
104.
Text
=
"{Binding activeJob.Qualifications, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnExceptions=True}"
105.
Grid.ColumnSpan
=
"2"
/>
106.
<!-- Requirements Checkboxes-->
107.
108.
<
CheckBox
x:Name
=
"xResumeRequiredCB"
Margin
=
"8,8,8,15"
109.
Content
=
"Resume Required"
110.
Grid.Row
=
"6"
111.
Grid.ColumnSpan
=
"2"
112.
IsChecked
=
"{Binding activeJob.NeedsResume, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnExceptions=True}"
/>
113.
114.
<
CheckBox
x:Name
=
"xCoverletterRequiredCB"
Margin
=
"8,8,8,15"
115.
Content
=
"Cover Letter Required"
116.
Grid.Column
=
"2"
117.
Grid.Row
=
"6"
118.
IsChecked
=
"{Binding activeJob.NeedsCV, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnExceptions=True}"
/>
119.
120.
<
CheckBox
x:Name
=
"xOverviewRequiredCB"
Margin
=
"8,8,8,15"
121.
Content
=
"Overview Required"
122.
Grid.Row
=
"7"
123.
Grid.ColumnSpan
=
"2"
124.
IsChecked
=
"{Binding activeJob.NeedsOverview, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnExceptions=True}"
/>
125.
126.
<
CheckBox
x:Name
=
"xJobActiveCB"
Margin
=
"8,8,8,15"
127.
Content
=
"Job is Active"
128.
Grid.Column
=
"2"
129.
Grid.Row
=
"7"
130.
IsChecked
=
"{Binding activeJob.IsActive, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnExceptions=True}"
/>
131.
132.
<!-- Buttons -->
133.
134.
<
Button
x:Name
=
"xAddEditButton"
Margin
=
"8,8,0,10"
135.
Content
=
"{Binding AddEditButtonString}"
136.
cal:Click.Command
=
"{Binding AddEditCommand}"
137.
Grid.Column
=
"2"
138.
Grid.Row
=
"8"
139.
HorizontalAlignment
=
"Left"
140.
Width
=
"125"
141.
telerik:StyleManager.Theme
=
"Windows7"
/>
142.
143.
<
Button
x:Name
=
"xCancelButton"
HorizontalAlignment
=
"Right"
144.
Content
=
"Cancel"
145.
cal:Click.Command
=
"{Binding CancelCommand}"
146.
Margin
=
"0,8,8,10"
147.
Width
=
"125"
148.
Grid.Column
=
"2"
149.
Grid.Row
=
"8"
150.
telerik:StyleManager.Theme
=
"Windows7"
/>
151.
</
Grid
>
152.
</
Grid
>
The 'validationHelper:ValidationScope' line may seem odd. This is a handy little trick for catching current and would-be validation errors when working in this whole setup. This all comes from an approach found on the Joy Of Code blog, although it looks like the story for this will be changing slightly with new advances in SL4/WCF RIA Services, so this section can definitely get an overhaul a little down the road.
The code is the fun part of all this, so let us see what's happening under the hood.
Viewmodel for the AddEditJobView
We are going to see some of the same things happening here, so I'll skip over the repeat info and get right to the good stuff:
001.
public
class
AddEditJobViewModel : ViewModelBase
002.
{
003.
private
readonly
IEventAggregator eventAggregator;
004.
private
readonly
IRegionManager regionManager;
005.
006.
public
RecruitingContext context;
007.
008.
private
JobPosting _activeJob;
009.
public
JobPosting activeJob
010.
{
011.
get
{
return
_activeJob; }
012.
set
013.
{
014.
if
(_activeJob != value)
015.
{
016.
_activeJob = value;
017.
NotifyChanged(
"activeJob"
);
018.
}
019.
}
020.
}
021.
022.
public
bool
isNewJob;
023.
024.
private
string
_addEditString;
025.
public
string
AddEditString
026.
{
027.
get
{
return
_addEditString; }
028.
set
029.
{
030.
if
(_addEditString != value)
031.
{
032.
_addEditString = value;
033.
NotifyChanged(
"AddEditString"
);
034.
}
035.
}
036.
}
037.
038.
private
string
_addEditButtonString;
039.
public
string
AddEditButtonString
040.
{
041.
get
{
return
_addEditButtonString; }
042.
set
043.
{
044.
if
(_addEditButtonString != value)
045.
{
046.
_addEditButtonString = value;
047.
NotifyChanged(
"AddEditButtonString"
);
048.
}
049.
}
050.
}
051.
052.
public
SubscriptionToken addJobToken =
new
SubscriptionToken();
053.
public
SubscriptionToken editJobToken =
new
SubscriptionToken();
054.
055.
public
DelegateCommand<
object
> AddEditCommand {
get
;
set
; }
056.
public
DelegateCommand<
object
> CancelCommand {
get
;
set
; }
057.
058.
private
ObservableCollection<ValidationError> _errors =
new
ObservableCollection<ValidationError>();
059.
public
ObservableCollection<ValidationError> Errors
060.
{
061.
get
{
return
_errors; }
062.
}
063.
064.
private
ObservableCollection<ValidationResult> _valResults =
new
ObservableCollection<ValidationResult>();
065.
public
ObservableCollection<ValidationResult> ValResults
066.
{
067.
get
{
return
this
._valResults; }
068.
}
069.
070.
public
AddEditJobViewModel(IEventAggregator eventAgg, IRegionManager regionmanager)
071.
{
072.
// set Unity items
073.
this
.eventAggregator = eventAgg;
074.
this
.regionManager = regionmanager;
075.
076.
context =
new
RecruitingContext();
077.
078.
AddEditCommand =
new
DelegateCommand<
object
>(
this
.AddEditJobCommand);
079.
CancelCommand =
new
DelegateCommand<
object
>(
this
.CancelAddEditCommand);
080.
081.
SetSubscriptions();
082.
}
083.
084.
#region Subscription Declaration and Events
085.
086.
public
void
SetSubscriptions()
087.
{
088.
AddJobEvent addJob =
this
.eventAggregator.GetEvent<AddJobEvent>();
089.
090.
if
(addJobToken !=
null
)
091.
addJob.Unsubscribe(addJobToken);
092.
093.
addJobToken = addJob.Subscribe(
this
.AddJobEventHandler);
094.
095.
EditJobEvent editJob =
this
.eventAggregator.GetEvent<EditJobEvent>();
096.
097.
if
(editJobToken !=
null
)
098.
editJob.Unsubscribe(editJobToken);
099.
100.
editJobToken = editJob.Subscribe(
this
.EditJobEventHandler);
101.
}
102.
103.
public
void
AddJobEventHandler(
bool
isNew)
104.
{
105.
this
.activeJob =
null
;
106.
this
.activeJob =
new
JobPosting();
107.
this
.activeJob.IsActive =
true
;
// We assume that we want a new job to go up immediately
108.
this
.isNewJob =
true
;
109.
this
.AddEditString =
"Add New Job Posting"
;
110.
this
.AddEditButtonString =
"Add Job"
;
111.
112.
MakeMeActive(
this
.regionManager,
"MainRegion"
,
"AddEditJobView"
);
113.
}
114.
115.
public
void
EditJobEventHandler(JobPosting editJob)
116.
{
117.
this
.activeJob =
null
;
118.
this
.activeJob = editJob;
119.
this
.isNewJob =
false
;
120.
this
.AddEditString =
"Edit Job Posting"
;
121.
this
.AddEditButtonString =
"Edit Job"
;
122.
123.
MakeMeActive(
this
.regionManager,
"MainRegion"
,
"AddEditJobView"
);
124.
}
125.
126.
#endregion
127.
128.
#region DelegateCommands from View
129.
130.
public
void
AddEditJobCommand(
object
obj)
131.
{
132.
if
(
this
.Errors.Count > 0)
133.
{
134.
List<
string
> errorMessages =
new
List<
string
>();
135.
136.
foreach
(var valR
in
this
.Errors)
137.
{
138.
errorMessages.Add(valR.Exception.Message);
139.
}
140.
141.
this
.eventAggregator.GetEvent<DisplayValidationErrorsEvent>().Publish(errorMessages);
142.
143.
}
144.
else
if
(!Validator.TryValidateObject(
this
.activeJob,
new
ValidationContext(
this
.activeJob,
null
,
null
), _valResults,
true
))
145.
{
146.
List<
string
> errorMessages =
new
List<
string
>();
147.
148.
foreach
(var valR
in
this
._valResults)
149.
{
150.
errorMessages.Add(valR.ErrorMessage);
151.
}
152.
153.
this
._valResults.Clear();
154.
155.
this
.eventAggregator.GetEvent<DisplayValidationErrorsEvent>().Publish(errorMessages);
156.
}
157.
else
158.
{
159.
if
(
this
.isNewJob)
160.
{
161.
this
.eventAggregator.GetEvent<AddJobCompleteEvent>().Publish(
this
.activeJob);
162.
}
163.
else
164.
{
165.
this
.eventAggregator.GetEvent<EditJobCompleteEvent>().Publish(
true
);
166.
}
167.
}
168.
}
169.
170.
public
void
CancelAddEditCommand(
object
obj)
171.
{
172.
if
(
this
.isNewJob)
173.
{
174.
this
.eventAggregator.GetEvent<AddJobCompleteEvent>().Publish(
null
);
175.
}
176.
else
177.
{
178.
this
.eventAggregator.GetEvent<EditJobCompleteEvent>().Publish(
false
);
179.
}
180.
}
181.
182.
#endregion
183.
}
184.
}
We start seeing something new on line 103- the AddJobEventHandler will create a new job and set that to the activeJob item on the ViewModel. When this is all set, the view calls that familiar MakeMeActive method to activate itself. I made a bit of a management call on making views self-activate like this, but I figured it works for one reason. As I create this application, views may not exist that I have in mind, so after a view receives its 'ping' from being subscribed to an event, it prepares whatever it needs to do and then goes active. This way if I don't have 'edit' hooked up, I can click as the day is long on the main view and won't get lost in an empty region. Total personal preference here. :)
Everything else should again be pretty straightforward, although I do a bit of validation checking in the AddEditJobCommand, which can either fire off an event back to the main view/viewmodel if everything is a success or sent a list of errors to our notification module, which pops open a RadWindow with the alerts if any exist.
As a bonus side note, here's what my WCF RIA Services metadata looks like for handling all of the validation:
private
JobPostingMetadata()
{
}
[StringLength(2500, ErrorMessage =
"Description should be more than one and less than 2500 characters."
, MinimumLength = 1)]
[Required(ErrorMessage =
"Description is required."
)]
public
string
Description;
[Required(ErrorMessage=
"Active Status is Required"
)]
public
bool
IsActive;
[StringLength(100, ErrorMessage =
"Posting title must be more than 3 but less than 100 characters."
, MinimumLength = 3)]
[Required(ErrorMessage =
"Job Title is required."
)]
public
bool
JobTitle;
[Required]
public
string
Location;
public
bool
NeedsCV;
public
bool
NeedsOverview;
public
bool
NeedsResume;
public
int
PostingID;
[Required(ErrorMessage=
"Qualifications are required."
)]
[StringLength(2500, ErrorMessage=
"Qualifications should be more than one and less than 2500 characters."
, MinimumLength=1)]
public
string
Qualifications;
[StringLength(2500, ErrorMessage =
"Requirements should be more than one and less than 2500 characters."
, MinimumLength = 1)]
[Required(ErrorMessage=
"Requirements are required."
)]
public
string
Requirements;
The RecruitCB Alternative
See all that Xaml I pasted above? Those are now two pieces sitting in the JobsView.xaml file now. The only real difference is that the xEditGrid now sits in the same place as xJobsGrid, with visibility swapping out between the two for a quick switch. I also took out all the cal: and command: command references and replaced Button events with clicks and the Grid selection command replaced with a SelectedItemChanged event. Also, at the bottom of the xEditGrid after the last button, I add a ValidationSummary (with Visibility=Collapsed) to catch any errors that are popping up. Simple as can be, and leads to this being the single code-behind file:
001.
public
partial
class
JobsView : UserControl
002.
{
003.
public
RecruitingContext context;
004.
public
JobPosting activeJob;
005.
public
bool
isNew;
006.
private
ObservableCollection<ValidationResult> _valResults =
new
ObservableCollection<ValidationResult>();
007.
public
ObservableCollection<ValidationResult> ValResults
008.
{
009.
get
{
return
this
._valResults; }
010.
}
011.
public
JobsView()
012.
{
013.
InitializeComponent();
014.
this
.Loaded +=
new
RoutedEventHandler(JobsView_Loaded);
015.
}
016.
void
JobsView_Loaded(
object
sender, RoutedEventArgs e)
017.
{
018.
context =
new
RecruitingContext();
019.
xJobsGrid.ItemsSource = context.JobPostings;
020.
context.Load(context.GetJobPostingsQuery());
021.
}
022.
private
void
xAddRecordButton_Click(
object
sender, RoutedEventArgs e)
023.
{
024.
activeJob =
new
JobPosting();
025.
isNew =
true
;
026.
xAddEditTitle.Text =
"Add a Job Posting"
;
027.
xAddEditButton.Content =
"Add"
;
028.
xEditGrid.DataContext = activeJob;
029.
HideJobsGrid();
030.
}
031.
private
void
xEditRecordButton_Click(
object
sender, RoutedEventArgs e)
032.
{
033.
activeJob = xJobsGrid.SelectedItem
as
JobPosting;
034.
isNew =
false
;
035.
xAddEditTitle.Text =
"Edit a Job Posting"
;
036.
xAddEditButton.Content =
"Edit"
;
037.
xEditGrid.DataContext = activeJob;
038.
HideJobsGrid();
039.
}
040.
private
void
xAddEditButton_Click(
object
sender, RoutedEventArgs e)
041.
{
042.
if
(!Validator.TryValidateObject(
this
.activeJob,
new
ValidationContext(
this
.activeJob,
null
,
null
), _valResults,
true
))
043.
{
044.
List<
string
> errorMessages =
new
List<
string
>();
045.
foreach
(var valR
in
this
._valResults)
046.
{
047.
errorMessages.Add(valR.ErrorMessage);
048.
}
049.
this
._valResults.Clear();
050.
ShowErrors(errorMessages);
051.
}
052.
else
if
(xSummary.Errors.Count > 0)
053.
{
054.
List<
string
> errorMessages =
new
List<
string
>();
055.
foreach
(var err
in
xSummary.Errors)
056.
{
057.
errorMessages.Add(err.Message);
058.
}
059.
ShowErrors(errorMessages);
060.
}
061.
else
062.
{
063.
if
(
this
.isNew)
064.
{
065.
context.JobPostings.Add(activeJob);
066.
context.SubmitChanges((s) =>
067.
{
068.
ActionHistory thisAction =
new
ActionHistory();
069.
thisAction.PostingID = activeJob.PostingID;
070.
thisAction.Description = String.Format(
"Job '{0}' has been edited by {1}"
, activeJob.JobTitle,
"default user"
);
071.
thisAction.TimeStamp = DateTime.Now;
072.
context.ActionHistories.Add(thisAction);
073.
context.SubmitChanges();
074.
},
null
);
075.
}
076.
else
077.
{
078.
context.SubmitChanges((s) =>
079.
{
080.
ActionHistory thisAction =
new
ActionHistory();
081.
thisAction.PostingID = activeJob.PostingID;
082.
thisAction.Description = String.Format(
"Job '{0}' has been added by {1}"
, activeJob.JobTitle,
"default user"
);
083.
thisAction.TimeStamp = DateTime.Now;
084.
context.ActionHistories.Add(thisAction);
085.
context.SubmitChanges();
086.
},
null
);
087.
}
088.
ShowJobsGrid();
089.
}
090.
}
091.
private
void
xCancelButton_Click(
object
sender, RoutedEventArgs e)
092.
{
093.
ShowJobsGrid();
094.
}
095.
private
void
ShowJobsGrid()
096.
{
097.
xAddEditRecordButtonPanel.Visibility = Visibility.Visible;
098.
xEditGrid.Visibility = Visibility.Collapsed;
099.
xJobsGrid.Visibility = Visibility.Visible;
100.
}
101.
private
void
HideJobsGrid()
102.
{
103.
xAddEditRecordButtonPanel.Visibility = Visibility.Collapsed;
104.
xJobsGrid.Visibility = Visibility.Collapsed;
105.
xEditGrid.Visibility = Visibility.Visible;
106.
}
107.
private
void
ShowErrors(List<
string
> errorList)
108.
{
109.
string
nm =
"Errors received: \n"
;
110.
foreach
(
string
anerror
in
errorList)
111.
nm += anerror +
"\n"
;
112.
RadWindow.Alert(nm);
113.
}
114.
}
The first 39 lines should be pretty familiar, not doing anything too unorthodox to get this up and running. Once we hit the xAddEditButton_Click on line 40, we're still doing pretty much the same things except instead of checking the ValidationHelper errors, we both run a check on the current activeJob object as well as check the ValidationSummary errors list. Once that is set, we again use the callback of context.SubmitChanges (lines 68 and 78) to create an ActionHistory which we will use to track these items down the line.
That's all?
Essentially... yes. If you look back through this post, most of the code and adventures we have taken were just to get things working in the MVVM/Prism setup. Since I have the whole 'module' self-contained in a single JobView+code-behind setup, I don't have to worry about things like sending events off into space for someone to pick up, communicating through an Infrastructure project, or even re-inventing events to be used with attached behaviors. Everything just kinda works, and again with much less code. Here's a picture of the MVVM and Code-behind versions on the Jobs and AddEdit views, but since the functionality is the same in both apps you still cannot tell them apart (for two-strike):
Looking ahead, the Applicants module is effectively the same thing as the Jobs module, so most of the code is being cut-and-pasted back and forth with minor tweaks here and there. So that one is being taken care of by me behind the scenes. Next time, we get into a new world of fun- the interview scheduling module, which will pull from available jobs and applicants for each interview being scheduled, tying everything together with RadScheduler to the rescue.
Evan Hutnick works as a Developer Evangelist for Telerik specializing in Silverlight and WPF in addition to being a Microsoft MVP for Silverlight. After years as a development enthusiast in .Net technologies, he has been able to excel in XAML development helping to provide samples and expertise in these cutting edge technologies. You can find him on Twitter @EvanHutnick.