Programmatically editing the hosts file

If you deal with multiple web servers, all with the same service addresses, but in different locations, you’ll find you’re constantly in your hosts file. It is not exactly an onerous task, changing the hosts file; typically, it resides in

C:\Windows\System32\drivers\etc

You have to be running as admin to make any changes, but other than that, it’s just a text file; mine looks like this:

# Copyright (c) 1993-2009 Microsoft Corp.
#
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
#
# This file contains the mappings of IP addresses to host names. Each
# entry should be kept on an individual line. The IP address should
# be placed in the first column followed by the corresponding host name.
# The IP address and the host name should be separated by at least one
# space.
#
# Additionally, comments (such as these) may be inserted on individual
# lines or following the machine name denoted by a ‘#’ symbol.
#
# For example:
#
# 102.54.94.97 rhino.acme.com # source server
# 38.25.63.10 x.acme.com # x client host

# localhost name resolution is handled within DNS itself.
# 127.0.0.1 localhost
# ::1 localhost
127.0.0.1 http://www.myuri.test
#192.168.1.1 http://www.myuri.test

The important part for me are the last two lines. As you can see, I have two potential IP addresses for the same URL. I obviously could simply change this in the hosts file; which, as I said, is not exactly a difficult task. But what if there are another 25 possible I.P. addresses? You could certainly have comments, or you could use the following code to create a small utility to change this for you.

I’ve put all the code for this into a single XAML window (using the code behind); this means that I’ve tightly coupled the logic to the UI. I’m a big fan of the MVVM pattern, but there is such a thing as over thinking a problem, and given that this is changing a Windows text file, putting the code into some form of MVVM framework seems overkill.

The XAML looks like this:

<Window x:Class="ChangeHosts.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ChangeHosts"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Grid>
        <StackPanel>
            <Button x:Name="LoadHosts" Click="LoadHosts_Click" Content="Load File"/>
            <ComboBox x:Name="comboHosts" ItemsSource="{Binding HostSelections}" SelectedIndex="{Binding HostSelectedIdx}"/>
            <Button x:Name="SetActiveHost" Click="SetActiveHost_Click" Content="Save Selection"/>
        </StackPanel>
    </Grid>
</Window>

As you can see, the first thing that I’ve done is set the data context to the code behind. Obviously, this means that the code-behind will need to implement INotifyPropertyChanged:

    public partial class MainWindow : Window, INotifyPropertyChanged
    {

We’re going to need a collection called HostSelections and an index to represent the selected item; here they are:

        private List<string> _hostSelections = new List<string>();
        public List<string> HostSelections
        {
            get { return _hostSelections; }
            set
            {
                _hostSelections = value;
                NotifyPropertyChanged();
            }
        }

        private int _previousSelectedIdx = -1;
        private int _hostSelectedIdx;
        public int HostSelectedIdx
        {
            get { return _hostSelectedIdx; }
            set
            {
                if (_hostSelectedIdx != _previousSelectedIdx || _previousSelectedIdx == -1)
                {
                    // Update previous value
                    _previousSelectedIdx = _hostSelectedIdx;

                    // Now update the selected index
                    _hostSelectedIdx = value;
                    NotifyPropertyChanged();
                }
            }
        }

The reason for tracking the previous value will become apparent in a minute. Other than that, there’s nothing really to see here. There are two event handlers required by the XAML; the first loads the hosts file:

        private async void LoadHosts_Click(object sender, RoutedEventArgs e)
        {
            await LoadHostFile();
        }

        private async Task LoadHostFile()
        {
            using (StreamReader sr = File.OpenText(System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "drivers/etc/hosts")))
            {
                string hosts = await sr.ReadToEndAsync();
                string[] entries = hosts.Split(new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries).ToArray();
                HostSelections = entries.ToList();

                int idxSel = entries.Select((str, idx) => new { str, idx }).FirstOrDefault(e => !e.str.StartsWith("#")).idx;
                HostSelectedIdx = idxSel;
            }
        }

So, we parse the text and then set the bound collection to the list. Because the file is reloaded every time, there’s no need for an observable collection here. The next event handler sets the new active entry:

        private async void SetActiveHost_Click(object sender, RoutedEventArgs e)
        {
            await ChangeSelecteHost();
        }

        private async Task ChangeSelecteHost()
        {
            if (HostSelections[HostSelectedIdx].StartsWith("#"))
            {
                HostSelections[HostSelectedIdx] = HostSelections[HostSelectedIdx].TrimStart('#');
                HostSelections[_previousSelectedIdx] = $"#{HostSelections[_previousSelectedIdx]}";

                WriteHostsFile();
                await LoadHostFile();
            }
            else
            {
                MessageBox.Show("The selected line is already selected");
            }
        }

        private void WriteHostsFile()
        {
            string updatedHosts = string.Join(Environment.NewLine, HostSelections);

            File.WriteAllText(System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), 
                "drivers/etc/hosts"), updatedHosts);
        }

As you can see, setting the previous index was needed here.

That’s it – the code works as is (I’m omitted the NotifyPropertyChanged boiler plate code).

Conclusion

I’ve uploaded this project to GitHub. Feel free to make suggestions, or submit a pull request.

Leave a Reply

Your email address will not be published. Required fields are marked *