38
\$\begingroup\$

I've made a Windows Forms application to track all of the processes running on my machine and it also saves the time an application is "active", an active application is the one that is on focus currently e.g your browser right now + it also reminds me every now and then (every hour) how much time I've spent on the internet.

Aside from that it shows all of your running processes with some basic information about them, there are also several different sorting options along with ascending/descending ordering.

Here's how it looks:

enter image description here Update

This line - winforms is not powerful seems to have caused quite some controversy and apparently I'm wrong. As pointed by CodyGray and proven by t3chb0t, windows forms can be really fast if the program is optimized properly and the controls are used the way they are meant to be used.

It works on a single thread and winforms is not powerful so it takes 1-2 seconds to refresh the content which happens every 10 seconds, unless requested manually.

Here is the main code:

public partial class Form1 : Form
{
    private class ProcessInfo
    {
        public Process Process { get; }
        public TimeSpan TimeActive { get; set; }

        public ProcessInfo(Process process, TimeSpan timeActive)
        {
            Process = process;
            TimeActive = timeActive;
        }
    }

    private readonly Timer updateTimer = new Timer();
    private readonly Timer focusTimeTimer = new Timer();

    private Dictionary<int, Process> processesInfo = new Dictionary<int, Process>();
    private List<KeyValuePair<int, Process>> orderedProcessesInfo;

    private Dictionary<string, Action> sortingActions;

    private Dictionary<string, Action> orderingActions;

    private bool isAscendingOrder = false;

    private static Dictionary<int, ProcessInfo> processesActiveTime = new Dictionary<int, ProcessInfo>();

    private static readonly Func<Process, int> GetMemoryUsageInMB = p => (int) (p.WorkingSet64 / (1024 * 1024));
    private static readonly Func<Process, TimeSpan> GetRuntimeOfProcess = p => DateTime.Now - p.StartTime;
    private static readonly Func<Process, TimeSpan> GetActiveTimeOfProcess = p => processesActiveTime[p.Id].TimeActive;

    //save state after update
    private string lastSortAction = string.Empty;

    public Form1()
    {
        InitializeComponent();
        LoadProcesses();
        InitializeSortingActions();
        InitializeOrderingActions();
        UpdateProcessList();

        updateTimer.Interval = 1000 * 10;
        updateTimer.Tick += UpdateTimer_Tick;
        updateTimer.Start();

        focusTimeTimer.Interval = 1000;
        focusTimeTimer.Tick += FocusTimeTimer_Tick;
        focusTimeTimer.Start();
    }

    private void FocusTimeTimer_Tick(object sender, EventArgs e)
    {
        TextBoxProcessCount.Text = processesInfo.Count.ToString();
        IntPtr activatedHandle = GetForegroundWindow();
        if (activatedHandle == IntPtr.Zero)
        {
            return;
        }
        int activeProcessId;
        GetWindowThreadProcessId(activatedHandle, out activeProcessId);
        ProcessInfo activeProcess;
        if (processesActiveTime.TryGetValue(activeProcessId, out activeProcess))
        {
            activeProcess.TimeActive =
                activeProcess.TimeActive.Add(new TimeSpan(0, 0, focusTimeTimer.Interval / 1000));
            if (activeProcess.TimeActive.Seconds == 0 && activeProcess.TimeActive.Minutes == 0 &&
                activeProcess.TimeActive.TotalHours > 0)
            {
                MessageBox.Show(
                    $@"You've spent {activeProcess.TimeActive.TotalHours} on {activeProcess.Process.ProcessName}");
            }
        }
        else
        {
            LoadProcesses();
            UpdateProcessList();
        }
    }

    private void LoadProcesses()
    {
        if (processesActiveTime.Count > 0)
        {
            try
            {
                processesActiveTime =
                    processesActiveTime.Where(p => !p.Value.Process.HasExited)
                        .ToDictionary(pair => pair.Key, pair => pair.Value);
            }
            catch (InvalidOperationException) { }
        }

        processesInfo.Clear();
        Process[] allProcesses = Process.GetProcesses();
        foreach (var process in allProcesses)
        {
            try
            {
                //ensures process wont deny access
                if (!process.HasExited)
                {
                    DateTime runtime = process.StartTime;
                }
            }
            catch (Win32Exception)
            {
                continue;
            }
            try
            {
                //ensures process wont exit
                processesInfo.Add(process.Id, process);
                if (!processesActiveTime.ContainsKey(process.Id))
                {
                    processesActiveTime.Add(process.Id, new ProcessInfo(process, new TimeSpan()));
                }
            }
            catch (InvalidOperationException) { }
        }
        orderedProcessesInfo = processesInfo.ToList();
    }

    private void InitializeSortingActions()
    {
        sortingActions = new Dictionary<string, Action>
        {
            ["Name"] = () => SortProcesses(p => p.ProcessName),
            ["Status"] = () => SortProcesses(p => p.Responding),
            ["Start Time"] = () => SortProcesses(p => p.StartTime),
            ["Total Runtime"] = () => SortProcesses(p => GetRuntimeOfProcess(p)),
            ["Memory Usage"] = () => SortProcesses(p => GetMemoryUsageInMB(p)),
            ["Active Time"] = () => SortProcesses(p => GetActiveTimeOfProcess(p))
        };

        foreach (var sortingAction in sortingActions)
        {
            ComboBoxSorting.Items.Add(sortingAction.Key);
        }
    }

    private void InitializeOrderingActions()
    {
        orderingActions = new Dictionary<string, Action>
        {
            ["Ascending"] = () =>
            {
                isAscendingOrder = true;
                if (!string.IsNullOrEmpty(lastSortAction))
                {
                    sortingActions[lastSortAction].Invoke();
                }
            },
            ["Descending"] = () =>
            {
                isAscendingOrder = false;
                if (!string.IsNullOrEmpty(lastSortAction))
                {
                    sortingActions[lastSortAction].Invoke();
                }
            },
        };
        foreach (var orderingAction in orderingActions)
        {
            ComboBoxOrders.Items.Add(orderingAction.Key);
        }
    }

    private void SortProcesses<T>(Expression<Func<Process, T>> lambda)
        where T : IComparable
    {
        orderedProcessesInfo.RemoveAll(p => p.Value.HasExited);

        orderedProcessesInfo.Sort(
            (process1, process2) =>
                lambda.Compile()
                    .Invoke(process1.Value).CompareTo(lambda.Compile()
                        .Invoke(process2.Value)));
        if (isAscendingOrder)
        {
            orderedProcessesInfo.Reverse();
        }
        processesInfo = orderedProcessesInfo.ToDictionary(pair => pair.Key, pair => pair.Value);

        UpdateProcessList();
    }

    private void UpdateTimer_Tick(object sender, EventArgs e)
    {
        RefreshList();
    }

    public void UpdateProcessList()
    {
        //refresh the timer's interval
        updateTimer.Stop();
        updateTimer.Start();

        ListViewProcesses.Clear();

        ListViewProcesses.Columns.Add("Name".ExtendWithEmptySpaces(GetAverageLengthOf(p => p.ProcessName.Length)));
        ListViewProcesses.Columns.Add("Status");
        ListViewProcesses.Columns.Add("Total Runtime");
        ListViewProcesses.Columns.Add("Active Runtime");
        ListViewProcesses.Columns.Add(
            "Start time".ExtendWithEmptySpaces(GetAverageLengthOf(p => p.StartTime.ToString().Length)));
        ListViewProcesses.Columns.Add(
            "Memory Usage".ExtendWithEmptySpaces(GetAverageLengthOf(p => GetMemoryUsageInMB(p).ToString().Length)));
        ListViewProcesses.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent);
        ListViewProcesses.AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize);

        foreach (var processInfo in processesInfo)
        {
            TimeSpan runtime = GetRuntimeOfProcess(processInfo.Value);
            TimeSpan activeTime = GetActiveTimeOfProcess(processInfo.Value);
            ListViewProcesses.Items.Add(
                CreateListViewRow(
                    name: processInfo.Value.ProcessName,
                    status: processInfo.Value.Responding ? "Active" : "Not responding",
                    runtime: $"{(int) runtime.TotalHours} h : {runtime.Minutes} min",
                    activeTime: $"{(int) activeTime.TotalHours} h : {activeTime.Minutes} min",
                    startTime: processInfo.Value.StartTime.ToString("g"),
                    memoryUsage: GetMemoryUsageInMB(processInfo.Value) + " MB"));
        }
    }

    private void bUpdate_Click(object sender, EventArgs e)
    {
        RefreshList();
    }

    private void RefreshList()
    {
        LoadProcesses();
        if (!string.IsNullOrEmpty(lastSortAction))
        {
            sortingActions[lastSortAction].Invoke();
        }
        else
        {
            UpdateProcessList();
        }
    }

    private static ListViewItem CreateListViewRow(string name, string status, string runtime, string activeTime,
        string startTime, string memoryUsage)
        => new ListViewItem(new[] {name, status, runtime, activeTime, startTime, memoryUsage});

    private int GetAverageLengthOf(Func<Process, int> predicate)
        => (int) Math.Ceiling(processesInfo.Values.Where(p => !p.HasExited).Average(predicate.Invoke));

    private void ComboBoxSorting_SelectedIndexChanged(object sender, EventArgs e)
    {
        lastSortAction = ((Control) sender).Text;
        sortingActions[lastSortAction].Invoke();
    }

    private void ComboBoxOrders_SelectedIndexChanged(object sender, EventArgs e)
    {
        orderingActions[((Control)sender).Text].Invoke();
    }

    [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
    private static extern IntPtr GetForegroundWindow();

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern int GetWindowThreadProcessId(IntPtr handle, out int processId);
}

There is just 1 extension method here which helps me even out the width of the columns so they don't look empty but they aren't hiding too much content either :

public static class Extensions
{
    public static string ExtendWithEmptySpaces(this string mainString, int desiredLength)
    {
        if (mainString.Length == desiredLength)
        {
            return mainString;
        }
        StringBuilder extendedStringBuilder = new StringBuilder(mainString);
        for (int i = 0; i < desiredLength - mainString.Length; i++)
        {
            extendedStringBuilder.Append(" ");
        }
        return extendedStringBuilder.ToString();
    }
}

Feel free to use it, but keep in mind that there is no way to record how much time you spent on applications unless the program is opened.

\$\endgroup\$
11
  • 8
    \$\begingroup\$ "winforms is not powerful" This is not at all true. There are two reasons why WinForms applications are slow: (1) the code is written inefficiently (as with everything), (2) it is a .NET framework, and managed applications still run inherently slower than unmanaged, native-compiled applications. Many of the inefficiencies here are just standard C#/.NET inefficiencies and have nothing to do with WinForms itself. \$\endgroup\$ Jan 8, 2017 at 6:18
  • 2
    \$\begingroup\$ I'm a little triggered by your statement, @denis. WinForms is still powerful. \$\endgroup\$
    – delete me
    Jan 8, 2017 at 8:21
  • 1
    \$\begingroup\$ @t3chb0t So far I know there isn't any way around managed code if I want to use WinForms and C++, or am I wrong? If that's right WinForms using C++ should only run a bit faster than WinForms using C#. However WinForms do run very very fast, there shouldn't be any problem if people don't try stuff like mentioned in your answer (add(..) every refresh ._.). \$\endgroup\$ Jan 8, 2017 at 9:10
  • 1
    \$\begingroup\$ @t3chb0t I went through many GUI systems(java swing, javafx, Qt, wxWidgets, WinForms and WPF) and I can only agree that the raw OS functions for GUI function are the fastest. I draw Waveforms and I'm very strict about performance and after all, WinForms is perfect in case of performance. Conclusion: WinForms is not slow! \$\endgroup\$ Jan 8, 2017 at 9:17
  • 1
    \$\begingroup\$ The ListView flickers in WinForms. It is a bug. Lots of workarounds available online. Start here for sorting. Also see this blog post. I have my own implementation that fixes these bugs and more in a class library, so I can just use that instead. The other UI problems are caused by either inefficiently manipulating them or failing to use BeginUpdate/EndUpdate. If you do that correctly, you won't have a problem. No one needs 100+ controls on a form; that's a UX problem. \$\endgroup\$ Jan 8, 2017 at 16:00

4 Answers 4

51
\$\begingroup\$

Performance

[..] winforms is not powerful so it takes 1-2 seconds to refresh the content [..]

It's not WinForms because it's actually very fast and I have never had any issues with it. There is a 99.99% chance that the code is inefficient so let's have a look.

Expression.Compile()

This is what slows the application down and where the bottle-neck is hidden and where the Expressions bite.

private void SortProcesses<T>(Expression<Func<Process, T>> lambda)
  where T : IComparable
{
  orderedProcessesInfo.RemoveAll(p => p.Value.HasExited);

  orderedProcessesInfo.Sort(
      (process1, process2) =>
          lambda.Compile()
              .Invoke(process1.Value).CompareTo(lambda.Compile()
                  .Invoke(process2.Value)));
  if (isAscendingOrder)
  {
      orderedProcessesInfo.Reverse();
  }
  processesInfo = orderedProcessesInfo.ToDictionary(pair => pair.Key, pair => pair.Value);

  UpdateProcessList();
}

There are two (!) Compiles. They are very expensive and there is no need for them and the Expression because there is nothing that dynamically changes. You have always a Process and a value to get and to compare. Use just the Func:

private void SortProcesses<T>(Func<Process, T> getProperty)
    where T : IComparable
{

    // ...

    orderedProcessesInfo.Sort((process1, process2) =>
            getProperty(process1.Value)
            .CompareTo(getProperty(process2.Value))
    );

    // ...
}

This is a sort method and it needs to run fast. If it doesn't, you'll notice it right away.


Here's a link to @Eric's Lippert answer on Stack Overflow explaining what the Compile method does: What does Lambda Expression Compile() method do?.

And one more link to another answer (by someone else) on Stack Overflow comparing execution times of various method calls: Performance of Expression.Compile vs Lambda, direct vs virtual calls


Another thing that I don't like about it is this

orderedProcessesInfo.Reverse()

The Sort should already produce the right order. The above line looks like the sorting wouldn't work correctly and you need this workaround to fix it instead of fixing the sort function.


ListView.Items.Add()

The second method that most likely makes you think WinForms wouldn't be performing well is this one:

public void UpdateProcessList()

You call here

ListViewProcesses.Clear();

[..] this method removes all items and columns from the ListView control

only to immediately recreate the columns with

ListViewProcesses.Columns.Add(..);

Do you really want to do it each time you refresh the list? You have already created the list-view once. I guess what you really wanted to do is to just remove all the items with ListView.Items.Clear().

This and adding the many rows to the list in a loop without suspending it really hurt the performance because the list-view keeps refreshing after each change.

foreach (var processInfo in processesInfo)
{     
    // ..
    ListViewProcesses.Items.Add(..);
    // ..
}

Consider using the BeginUpdate and EndUpdate methods and adding the rows inbetween or better, use the AddRange method:

The preferred way to add multiple items to a ListView is to use the AddRange method of the ListView.ListViewItemCollection (accessed through the Items property of the ListView). This enables you to add an array of items to the list in a single operation. However, if you want to add items one at a time using the Add method of the ListView.ListViewItemCollection class, you can use the BeginUpdate method to prevent the control from repainting the ListView every time that an item is added. When you have completed the task of adding items to the control, call the EndUpdate method to enable the ListView to repaint. This way of adding items can prevent flickered drawing of the ListView when lots of items are being added to the control.

ListView.BeginUpdate Method


To further improve the performance you could try to derive your own list-view-item from the

public class ListViewItem

that luckily isn't sealed. Then instead of re-adding all items you could just refresh the items and the list-view could just update the values with ListView.Refresh(). To keep track of the list-view-items and processes you could use another dictionaray and if a new process is added or removed you add/remove just this one and not all of them.


Design

private Dictionary<string, Action> sortingActions;

private Dictionary<string, Action> orderingActions;

private bool isAscendingOrder = false;

I find these three fields very confusing because they sound so similar.

How about this. First rename this one

sortingActions -> selectColumn

With the new defintion you selects the column (value) without invoking sorting yet:

private Dictionary<string, Func<Process, IComparable>> selectColumn;

selectColumn = new Dictionary<string, Func<Process, IComparable>>
{
    ["Name"] = p => p.ProcessName,
    ["Status"] = p => p.Responding,
    ["Start Time"] = p => p.StartTime,
    ["Total Runtime"] = p => GetRuntimeOfProcess(p),
    ["Memory Usage"] = p => GetMemoryUsageInMB(p),
    ["Active Time"] = p => GetActiveTimeOfProcess(p)
};

Then rename the other dictionary to

orderingActions -> orderBy

where the key is no longer a string but an enum:

enum OrderBy
{
    Ascending,
    Descending
}

so the new dictionary has now stronger keys and its items trigger the sort function by using the first dictionary to get the delegate for getting the column (value)

orderBy = new Dictionary<OrderBy, Action>
{
    [OrderBy.Ascending] = () =>
    {
        SortProcesses(selectColumn[orderByColumn]);
        currentOrderBy = OrderBy.Ascending;
    },
    [..]
}

where orderByColumn is the name of the column to be ordered by that you set somewhere.

The isAscendingOrder now becomes currentOrderBy

private OrderBy currentOrderBy = OrderBy.Descending;

(disclaimer: notepad coding, may not be 100% correct yet)

\$\endgroup\$
5
  • 2
    \$\begingroup\$ Hey hey, nice one! Things you mention in the performance section are so clear to me, that I always ask myself why other people don't think about what such stuff as add(..) is doing and how many time it costs. [Edited] \$\endgroup\$ Jan 8, 2017 at 9:18
  • 8
    \$\begingroup\$ Indeed and that's the problem with C# (or rather those who code it), there's all this shiny black box stuff which makes it so easy to write cool-looking but inefficient code if you don't know what's going on underneath. \$\endgroup\$
    – 404
    Jan 8, 2017 at 10:57
  • \$\begingroup\$ Thank you for the great suggestions once again @t3chb0t, I removed the list.Reverse() and now i'm just comparing 2 -> 1 for ascending and 1 -> 2 for descending \$\endgroup\$
    – Denis
    Jan 8, 2017 at 14:36
  • 2
    \$\begingroup\$ Worth pointing out that if you want to remove all items but not columns, listview.Items.Clear() is preferable to listview.Clear(). \$\endgroup\$
    – IanF1
    Jan 8, 2017 at 15:09
  • 1
    \$\begingroup\$ @IanF1 You're right, I should have mentioned it from the beginning. Updated. \$\endgroup\$
    – t3chb0t
    Jan 8, 2017 at 15:16
15
\$\begingroup\$

First thing I would do is change if (mainString.Length == desiredLength) to if (mainString.Length >= desiredLength), since if it's longer it'll just do excess work with the StringBuilder and you may as well return early on all conditions that would satisfy it.


I would also combine this try/catch block:

try
{
    //ensures process wont deny access
    if (!process.HasExited)
    {
        DateTime runtime = process.StartTime;
    }
}
catch (Win32Exception)
{
    continue;
}
try
{
    //ensures process wont exit
    processesInfo.Add(process.Id, process);
    if (!processesActiveTime.ContainsKey(process.Id))
    {
        processesActiveTime.Add(process.Id, new ProcessInfo(process, new TimeSpan()));
    }
}
catch (InvalidOperationException) { }

To:

try
{
    //ensures process wont deny access
    if (!process.HasExited)
    {
        DateTime runtime = process.StartTime;
    }

    //ensures process wont exit
    processesInfo.Add(process.Id, process);
    if (!processesActiveTime.ContainsKey(process.Id))
    {
        processesActiveTime.Add(process.Id, new ProcessInfo(process, new TimeSpan()));
    }
}
catch (Win32Exception) { continue; }
catch (InvalidOperationException) { }

With expression-bodied members, I've found it's easier to read when the 'lambda' syntax is on the same line as the member.

private static ListViewItem CreateListViewRow(string name, string status, string runtime, string activeTime,
    string startTime, string memoryUsage)
    => new ListViewItem(new[] {name, status, runtime, activeTime, startTime, memoryUsage});

To:

private static ListViewItem CreateListViewRow(string name, string status, string runtime, string activeTime,
    string startTime, string memoryUsage) =>
    new ListViewItem(new[] {name, status, runtime, activeTime, startTime, memoryUsage});

Next, same block from above:

private static ListViewItem CreateListViewRow(string name, string status, string runtime, string activeTime,
    string startTime, string memoryUsage) =>
    new ListViewItem(new[] {name, status, runtime, activeTime, startTime, memoryUsage});

Break each parameter on a new line if you're going to break one of them:

private static ListViewItem CreateListViewRow(string name,
                                              string status,
                                              string runtime
                                              string activeTime,
                                              string startTime,
                                              string memoryUsage) =>
    new ListViewItem(new[] {name, status, runtime, activeTime, startTime, memoryUsage});

Once we do that we see that it's a bit ugly, this method isn't a place for expression-bodied members:

private static ListViewItem CreateListViewRow(string name,
                                              string status,
                                              string runtime
                                              string activeTime,
                                              string startTime,
                                              string memoryUsage)
{
    return new ListViewItem(new[]
    {
        name,
        status,
        runtime,
        activeTime,
        startTime, 
        memoryUsage
    });
}

I'll try to add more later, quite late right now and there's probably a lot more to say.

\$\endgroup\$
9
\$\begingroup\$

I added a few dummy rows in the listview and tried to refresh it a few times, to see how it looks. And guess what it was flickering and it was taking around 100ms maybe to refresh the list view. Which I consider slow, that's just 4-5 empty row refreshed come on, you can't call that fast. Try putting a lot of controls on your form it will give up eventually. The worst part about windows forms is probably the drawing of controls.

True, this would be terrible so here is one more quick & dirty example of a ListView being refreshed with 100 items every 0.5sec and each value is refreshed every 50ms. No flickering, no delays. It's fast as hell and it should be. If it's not then something's not right.

This can be run in LINQPad:

void Main()
{
    var form = new Form();

    var lv = new MyListView
    {
        Dock = DockStyle.Fill
    };
    lv.Columns.Add("Column1");
    lv.Columns.Add("Column2");
    lv.Columns.Add("Column3");
    lv.View = View.Details;
    form.Controls.Add(lv);
    lv.AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize);

    var itemsTimer = new System.Windows.Forms.Timer
    {
        Interval = 500,
    };

    var suspendLayout = true;
    var itemCount = 100;

    var valueTimer = new System.Windows.Forms.Timer
    {
        Interval = 50
    };

    itemsTimer.Tick += (sender, e) =>
    {
        valueTimer.Stop();
        if (suspendLayout) lv.BeginUpdate();
        lv.Items.Clear();
        for (int i = 0; i < itemCount; i++)
        {
            lv.Items.Add(new ListViewItem(new string[] { "Foo", "Bar", "Baz" }));
        }
        if (suspendLayout) lv.EndUpdate();
        valueTimer.Start();
    };
    itemsTimer.Start();


    var rnd = new Random();
    valueTimer.Tick += (sender, e) =>
    {
        if (suspendLayout) lv.BeginUpdate();
        for (int i = 0; i < itemCount; i++)
        {
            lv.Items[i].SubItems[rnd.Next(0, 3)].Text = rnd.Next(0, 10).ToString();
        }
        if (suspendLayout) lv.EndUpdate();
    };

    form.FormClosing += (sender, e) =>
    {
        valueTimer.Stop();
        itemsTimer.Stop();
    };

    form.Show();
}

class MyListView : ListView
{
    public MyListView() { DoubleBuffered = true; }
    protected override bool DoubleBuffered { get; set; }
}

ListView

\$\endgroup\$
7
\$\begingroup\$

Focusing only on:

public static class Extensions
{
    public static string ExtendWithEmptySpaces(this string mainString, int desiredLength)
    {
        if (mainString.Length == desiredLength)
        {
            return mainString;
        }
        StringBuilder extendedStringBuilder = new StringBuilder(mainString);
        for (int i = 0; i < desiredLength - mainString.Length; i++)
        {
            extendedStringBuilder.Append(" ");
        }
        return extendedStringBuilder.ToString();
    }
}

You have a public method which everyone can call so you should add some proper validation. You want to encapsulate the inner working of that method and therefore you don't want to expose implementation details about it.

What happens if mainString is null ? By accessing the Length property of the mainString an ArgumentNullException is thrown and the stacktrace will indicate exactly this which isn't what you want. You don't want to expose that you use the Length property of that string but only that mainString is null.

What happens if desiredLength is negative ? Nothing bad, but you should tell the caller of the code that he/she did something wrong by throwing an ArgumentOutOfRangeException otherwise a hidden bug in the callers code can occur.

The name of that extension method is somehow misleading. How can a space be empty ?

That being said, you are reinventing the wheel here, because there is already a well optimized .NET method namely String.PadRight(Int32) which you should use instead. Every decent programmer knows this method and wouldn't be surprised finding it in the code.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.