Modernizing .NETpad Step-By-Step, Part 8: Replace and Replace All (Premium)

Modernizing .NETpad Step-By-Step, Part 8: Replace and Replace All

In the previous article, we created a modern interface for Find/Replace and implemented Find, Find next, and Find previous. This time, we’ll wrap up the Find/Replace work by implementing Replace and Replace all, and migrating all the new Find/Replace code into the main app, fixing a few bugs as we go.

Implement Replace

In the OG version of .NETpad, I implemented the Replace function as a series of custom input box dialogs that prompted the user for the text to find and then the text to replace it with, in turn. It worked, barely, but the user interface was non-standard and there were all kinds of unhandled cases.

One goal of this new version is to address those issues, though the pane-based approach has its own limitations too. The Find/Replace pane stays on-screen and lets the user continue finding and replacing text, rather than just performing a one-off task and disappearing as before. This is good … and bad. But it’s what we have.

When we created the pane, we set it up so that only the Find-related controls would appear when the user engaged in a Find operation (currently by clicking a temporary menu item). So Replace should work similarly, by un-hiding the Replace-related controls so they can be accessed. That way, the Find and Replace options will work sort of like toggles.

(Should Replace and Replace all behave differently? That is, if Replace and Replace all are both visible if the user chooses Replace, does the menu part of the UI need to change? That’s an ongoing debate. For now, we will simply engage both as one, with the one difference being that the Replace button is the default when the user chooses that action.)

To get started, locate the ReplaceTestMenu_Click() event handler in MainWindow.xaml.cs. This is the event handler that fires when the user clicks the temporary Replace menu item, but it will eventually be wired to the Replace command. It’s currently empty.

Looking at FindTestMenu_Click(), it’s clear that we can simply just run that method first, since we would otherwise just duplicate most of that code. Then, we can un-hide the Replace-related controls (as that method hides them), change the default handler (what fires when the user taps Enter), and put the focus on the correct control. (There may be a more elegant way to do all this, but we’ll look at code cleanup and refactoring later.)

To test this properly, try a few things. Toggling between Find and Replace (by clicking those menu items). Selecting text and then choosing Replace. Not selecting text and then choosing Replace. And so on. For example, if you select text in the main app window’s text box and then click “Replace,” the Find/Replace pane should appear with all its controls, the selected text should appear in the Find text box, and the Replace text box should be selected so the user can type the replacement text.

OK, next we need to implement the Replace action associated with the Replace button. We can find the original code for this in the bottom of the OG ReplaceCommand_Executed() event handler.

As we did with Find, we can adapt this code to work in a Click() event handler for the Replace button. So we need to create that: Open MainWindow.xaml, locate the XAML code for ReplaceButton, and add a Click() event handler. While we’re here, create one for ReplaceAllButton as well.

Then, open MainWindow.xaml.cs and scroll down to the bottom, where you’ll find the two new event handlers.

In ReplaceButton_Click(), we’ll just use the code from ReplaceCommand_Executed() with some additions and a few minor changes to the original code. In testing this functionality, I found that I needed to explicitly assign the correct values to the app-level FindTextString and FindLastIndexFound variables, which are critical to all this, to ensure they are always correct. And in the minor changes department, we now reference the text in the FindTextBox and ReplaceTextBox controls directly instead of creating unnecessary temporary variables.

In testing the result, things seem to work OK for the most part. During testing, I added a little block of code at the end of ReplaceTestMenu_Click() that’s reflected in a previous shot that addressed a small issue with the original code. Now, if the Find text box doesn’t have any text in it (the characters being searched for), we’ll keep the default behavior from FindTestMenu_Click() (and put the focus on the Find text box).

There are likely still some bugs in this code, but it’s better than it was, and we have a quality pass to do later. So we can add that to the to-do list. For now, let’s move on to Replace all.

Implement Replace all

Replace all works a lot like Replace in the OG code: The only big difference is that it uses a Replace() method in the Visual Basic strings library to replace all the instances of the code.

So it stands to reason that the new version will look/work much like the new version of Replace (the action, as now found in ReplaceAllButton_Click()). I would like to put a Confirmation dialog up before this action is completed, as it can make numerous changes to the document, but looking at Notepad, I can see Microsoft doesn’t do that. So I’ll hold off for now. (But will likely add this later.)

Using ReplaceButton_Click() as the model–and understanding that there’s definitely some code consolidation opportunities there–creating ReplaceAllButton_Click() is mostly straightforward. The big difference is that bit inside the second if block.

The green squiggly there is related to a warning that it’s possible that line of code could try to convert a null value to a non-nullable type. There are a few solutions, but the path of least resistance is to use the ? annotation on the string declaration so that NewText can be nullable. Like so:

This works, but in testing this, I noticed an issue toggling between different Find and Replace states on the fly. So I added the following line of code to FindTestMenu_Click() to fix that.

I’m sure there are other issues, but for expediency’s sake–and because testing this with the real-world UIs is more important to do first–let’s move on. And that means migrating the new code into the main code base.

Migrate the code into the main code base

We’ve created temporary Click() event handlers for two temporary menu items, but now we will move that code where it belongs so that the normal app UIs work. In this case, that means the Find, Find next, Find previous, Replace, and Replace all menu items under the Edit menu. Plus all their associated keyboard shortcuts.

These are all (already) defined in the Windows.Resources (for keyboard shortcuts) and Window.CommandBindings (for command event handlers) blocks, respectively, near the top of the MainWindows.xaml file.

As you can see, each keyboard shortcut is tied (routed) to a specific command binding and each command binding is tied to a specific event handler. Likewise, if you find the XAML code for the related menu items, you can see that all of them except for “Find” reference the same command bindings and thus fire the same event handlers.

There’s nothing to do in the XAML, that’s not changing. But it should be pretty straightforward to move our new code into the original locations, respectively. So let’s do them in order.

Find

Locate FindCommand_Executed() in MainWindow.xaml.cs and delete its contents. Then, copy in the code from FindTestMenu_Click().

When you run the app now, typing Ctrl + F or selecting Edit > Find in the app menu should work normally, using the new Find/Replace pane.

Find next

We’re already using the correct event handler for this, meaning FindNextButton_Click(). But in keeping with my comment above, I’ve added explicit value assignments to the FindTextString and FindLastFoundIndex app-level variables. So that handler now looks like so.

But we have to handle the Find next command as well. There should be an empty FindNextCommand_Executed() event handler: It needs the same code as above. But rather than just copy and paste it, introducing a duplication, we can simply call FindNextButton_Click() from that event handler. It will look like this.

Be sure to test this using both the Find next (down arrow) button in the pane and using the F3 keyboard shortcut.

Find previous

Find previous is similar to Find next, of course, but with a small coding change. With the same small code additions made above, FindPreviousButton_Click() now looks like so:

And we can access FindPreviousButton_Click() from within FindPreviousCommand_Executed() as expected.

Test this one similarly to above. (The keyboard shortcut is Shift + F3.)

Replace

This is where things get interesting. We worked up the ReplaceButton_Click() event handler in this part of the project, above. That seems to work OK for the button, but being able to click that button means not only that the Find/Replace pane is visible, but that the Replace controls are visible too. And so we need to add some code to handle those instances if the user tries to access the Replace action using the menu or the Ctrl + H keyboard shortcut. Put another way, handling the Replace command execution isn’t the same thing as handling the user clicking the Replace button in the Find/Replace pane.

But they could be.

To make Replace work like Find, Find next, and Find previous, we can build out the code in ReplaceButton_Click() to handle both actions–the literal clicking of the Replace button and the execution of the Replace action. Then, we can do as before and execute ReplaceButton_Click() from within ReplaceCommand_Executed(). Maybe.

This requires a fairly extensive code addition to ReplaceButton_Click(). But it’s straightforward. If the Find/Replace pane is visible, we’ll run the code that is already in that event handler. If it’s not, we’ll display the Find/Replace pane and configure it correctly for the Replace actions.

Like so.

There’s just one problem: The line that calls FindCommand_Executed() in the if block has a type error. Here, too, there are many ways to solve this issue, but since we never reference the argument in question (e), we can simply pass a null value to that event handler instead.

That introduces a new warning, however: You can’t convert a null to a non-nullable reference type. The solution is to use that ? annotation on the argument in question in the definition of FindCommand_Executed(), like so:

Problem solved.

Now, find ReplaceCommand_Executed() and call ReplaceButton_Click(), consistent with our previous work.

Test as before, with the Find/Replace pane visible and not, and using all three trigger methods: The Edit > Replace menu item, the Ctrl + H keyboard shortcut, and the Replace button.

Replace all

Replace all follows the pattern established above: It’s a command and a button, so we need a single event handler that does both. In fact, it follows that pattern so exactly that we’re going to need to consolidate the if part of the if-else block in both event handlers (ReplaceButton_Click and ReplaceAllButton_Click)  to call the same method (which we’ll put in Backend.cs) to get rid of that duplication. This would normally be part of a code refactoring pass I’m planning for later (and did, earlier, in various other versions of this app I worked on over the summer). But I can’t stand leaving it as-is.

So, open Backend.cs and create a method called DisplayReplaceControls() like so:

Then, Cut the code out of the if block in ReplaceButton_Click() …

And paste it into DisplayReplaceControls() (in Backend.cs).

As you can see, there’s an error on the sender argument in the FindCommand_Executed() call. That’s because there’s no such thing as sender in this method. But since that argument is meant to communicate which control (or object) triggered the event handler, we can just send this, which represents the current object–most likely the containing app window–instead. We never use sender in FindCommand_Executed() anyway, so it doesn’t much matter.

Then, find that empty if block in ReplaceButton_Click() and add this code to run our new DisplayReplaceControls() method.

Test to make sure Replace still works as before (it does). And then we can do the same with ReplaceAllButton_Click(). It’s basically an if-else block that runs DisplayReplaceControls() if the Find/Replace pane isn’t visible, and then runs the code that was already there otherwise. It looks like this:

And, as expected, we then need to call ReplaceAllButton_Click() from ReplaceAllCommand_Executed():

And with that, it should all work … such as it is.

Remove the temporary menu items

Now, we can simply remove the temporary Find test and Replace test menu items we added in XAML (in MainWindow.xaml) and their corresponding Click() event handlers in MainWindow.xaml.cs. That’s easy enough. Then test, test, test.

Look to the future

We still have some code cleanup work to do, though I did some inline above because I couldn’t help myself. You might have also noticed that there are old-school Message boxes in there, too, and those are candidates for replacement using the custom Content dialog we created earlier. But this is enough for now: All the Find/Replace functionality should be working to some degree. It’s not as elegant as in the real Notepad. But it’s in a better place.

We’re almost done. The next phase should be our final phase, where we’ll clean up the code all around and make sure we haven’t missed anything. If all goes well–never a guarantee–we’ll be at that minimally acceptable point where the app has been modernized in meaningful ways and is better because of it. But there’s still plenty of room for further improvements, which can include those Notepad features we still don’t support, like tabs, fixing some style problems triggered by WPF limitations, and then some additional unique features we may or may not want to add.

But let’s not get ahead of ourselves. For now, Job One is cleaning up this mess and seeing where we stand.

Gain unlimited access to Premium articles.

With technology shaping our everyday lives, how could we not dig deeper?

Thurrott Premium delivers an honest and thorough perspective about the technologies we use and rely on everyday. Discover deeper content as a Premium member.

Tagged with

Share post

Thurrott