Telerik blogs

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:

Job Posting Solution Explorer Image

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:

  • Lines 11 and 16: Using the same type of click commands as we saw in the Menu module, we tie the button clicks to delegate commands in the viewmodel.
  • Line 25: The source for the jobs will be a collection in the viewmodel.
  • Line 26: We also bind the selected item to a public property from the viewmodel for use in code.
  • Line 27: We've turned the event into a command so we can handle it via code in the viewmodel.

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):

End of part 5

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.


About the Author

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.

Related Posts

Comments

Comments are disabled in preview mode.